“shubhamdhamal” commited on
Commit
a5cfef0
·
1 Parent(s): 9b44947

Fix: Include milestone progress in API responses for mobile app

Browse files
config.py CHANGED
@@ -9,28 +9,33 @@ if not os.environ.get('RENDER') and not os.environ.get('SPACE_ID'):
9
  # Set Flask app for CLI commands (needed for flask db upgrade)
10
  os.environ.setdefault('FLASK_APP', 'run.py')
11
 
 
12
  class Config:
13
  # Check if running in production (Render or HF Spaces)
14
- IS_PRODUCTION = bool(os.environ.get('RENDER') or os.environ.get('SPACE_ID'))
15
-
 
16
  # SECRET_KEY is CRITICAL for sessions and CSRF
17
- SECRET_KEY = os.environ.get('SECRET_KEY') or os.environ.get('FLASK_SECRET_KEY') or 'dev-secret-key-change-in-production-2024'
18
-
 
19
  # Database configuration - Use PostgreSQL (Neon) in production, SQLite locally
20
  # Set DATABASE_URL environment variable for production PostgreSQL connection
21
  # Example: postgresql://user:password@hostname/database?sslmode=require
22
- SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///learning_path.db'
23
-
 
24
  # Fix for Heroku/Render style postgres:// URLs (SQLAlchemy requires postgresql://)
25
  if SQLALCHEMY_DATABASE_URI and SQLALCHEMY_DATABASE_URI.startswith('postgres://'):
26
- SQLALCHEMY_DATABASE_URI = SQLALCHEMY_DATABASE_URI.replace('postgres://', 'postgresql://', 1)
27
-
 
28
  SQLALCHEMY_TRACK_MODIFICATIONS = False
29
  SQLALCHEMY_ENGINE_OPTIONS = {
30
  'pool_pre_ping': True, # Enable connection health checks
31
  'pool_recycle': 300, # Recycle connections every 5 minutes
32
  }
33
-
34
  # WTF CSRF Settings - Temporarily disabled due to HF Spaces session issues
35
  # TODO: Re-enable after figuring out session persistence
36
  WTF_CSRF_ENABLED = False # Disable CSRF for now - will re-enable with fix
@@ -42,11 +47,11 @@ class Config:
42
  SESSION_COOKIE_SAMESITE = 'Lax'
43
  PERMANENT_SESSION_LIFETIME = 7200 # 2 hours
44
  SESSION_COOKIE_NAME = 'learning_path_session'
45
-
46
  # HF Spaces internal traffic is HTTP even though external is HTTPS
47
  # Setting SECURE=False allows cookies to be set over internal HTTP
48
  SESSION_COOKIE_SECURE = False # Must be False for HF Spaces
49
  REMEMBER_COOKIE_SECURE = False
50
  REMEMBER_COOKIE_SAMESITE = 'Lax'
51
-
52
  LOG_TO_STDOUT = os.environ.get('LOG_TO_STDOUT')
 
9
  # Set Flask app for CLI commands (needed for flask db upgrade)
10
  os.environ.setdefault('FLASK_APP', 'run.py')
11
 
12
+
13
  class Config:
14
  # Check if running in production (Render or HF Spaces)
15
+ IS_PRODUCTION = bool(os.environ.get('RENDER')
16
+ or os.environ.get('SPACE_ID'))
17
+
18
  # SECRET_KEY is CRITICAL for sessions and CSRF
19
+ SECRET_KEY = os.environ.get('SECRET_KEY') or os.environ.get(
20
+ 'FLASK_SECRET_KEY') or 'dev-secret-key-change-in-production-2024'
21
+
22
  # Database configuration - Use PostgreSQL (Neon) in production, SQLite locally
23
  # Set DATABASE_URL environment variable for production PostgreSQL connection
24
  # Example: postgresql://user:password@hostname/database?sslmode=require
25
+ SQLALCHEMY_DATABASE_URI = os.environ.get(
26
+ 'DATABASE_URL') or 'sqlite:///learning_path.db'
27
+
28
  # Fix for Heroku/Render style postgres:// URLs (SQLAlchemy requires postgresql://)
29
  if SQLALCHEMY_DATABASE_URI and SQLALCHEMY_DATABASE_URI.startswith('postgres://'):
30
+ SQLALCHEMY_DATABASE_URI = SQLALCHEMY_DATABASE_URI.replace(
31
+ 'postgres://', 'postgresql://', 1)
32
+
33
  SQLALCHEMY_TRACK_MODIFICATIONS = False
34
  SQLALCHEMY_ENGINE_OPTIONS = {
35
  'pool_pre_ping': True, # Enable connection health checks
36
  'pool_recycle': 300, # Recycle connections every 5 minutes
37
  }
38
+
39
  # WTF CSRF Settings - Temporarily disabled due to HF Spaces session issues
40
  # TODO: Re-enable after figuring out session persistence
41
  WTF_CSRF_ENABLED = False # Disable CSRF for now - will re-enable with fix
 
47
  SESSION_COOKIE_SAMESITE = 'Lax'
48
  PERMANENT_SESSION_LIFETIME = 7200 # 2 hours
49
  SESSION_COOKIE_NAME = 'learning_path_session'
50
+
51
  # HF Spaces internal traffic is HTTP even though external is HTTPS
52
  # Setting SECURE=False allows cookies to be set over internal HTTP
53
  SESSION_COOKIE_SECURE = False # Must be False for HF Spaces
54
  REMEMBER_COOKIE_SECURE = False
55
  REMEMBER_COOKIE_SAMESITE = 'Lax'
56
+
57
  LOG_TO_STDOUT = os.environ.get('LOG_TO_STDOUT')
init_postgres_db.py CHANGED
@@ -6,6 +6,7 @@ Usage:
6
  1. Set DATABASE_URL environment variable
7
  2. Run: python init_postgres_db.py
8
  """
 
9
  import os
10
  import sys
11
  from dotenv import load_dotenv
@@ -32,28 +33,28 @@ if database_url.startswith('postgres://'):
32
  os.environ['DATABASE_URL'] = database_url
33
 
34
  print(f"🔗 Connecting to database...")
35
- print(f" Host: {database_url.split('@')[1].split('/')[0] if '@' in database_url else 'local'}")
 
36
 
37
  # Now import Flask app
38
- from web_app import create_app, db
39
 
40
  app = create_app()
41
 
42
  with app.app_context():
43
  print("📦 Creating all database tables...")
44
-
45
  # Import models to ensure they're registered
46
  from web_app.models import (
47
- User, UserLearningPath, LearningProgress,
48
  ResourceProgress, MilestoneProgress,
49
  ChatMessage, PathModification, ConversationSession, OAuth
50
  )
51
-
52
  # Create all tables
53
  db.create_all()
54
-
55
  print("✅ Database tables created successfully!")
56
-
57
  # Show created tables
58
  from sqlalchemy import inspect
59
  inspector = inspect(db.engine)
@@ -61,13 +62,13 @@ with app.app_context():
61
  print(f"\n📋 Tables in database ({len(tables)}):")
62
  for table in tables:
63
  print(f" - {table}")
64
-
65
  # Check if there are any users
66
  user_count = User.query.count()
67
  print(f"\n👥 Current users in database: {user_count}")
68
-
69
  if user_count == 0:
70
  print("\n💡 No users yet. Users will be created when they register via the app.")
71
-
72
  print("\n🎉 Database initialization complete!")
73
  print(" Your app is now ready to use persistent PostgreSQL storage.")
 
6
  1. Set DATABASE_URL environment variable
7
  2. Run: python init_postgres_db.py
8
  """
9
+ from web_app import create_app, db
10
  import os
11
  import sys
12
  from dotenv import load_dotenv
 
33
  os.environ['DATABASE_URL'] = database_url
34
 
35
  print(f"🔗 Connecting to database...")
36
+ print(
37
+ f" Host: {database_url.split('@')[1].split('/')[0] if '@' in database_url else 'local'}")
38
 
39
  # Now import Flask app
 
40
 
41
  app = create_app()
42
 
43
  with app.app_context():
44
  print("📦 Creating all database tables...")
45
+
46
  # Import models to ensure they're registered
47
  from web_app.models import (
48
+ User, UserLearningPath, LearningProgress,
49
  ResourceProgress, MilestoneProgress,
50
  ChatMessage, PathModification, ConversationSession, OAuth
51
  )
52
+
53
  # Create all tables
54
  db.create_all()
55
+
56
  print("✅ Database tables created successfully!")
57
+
58
  # Show created tables
59
  from sqlalchemy import inspect
60
  inspector = inspect(db.engine)
 
62
  print(f"\n📋 Tables in database ({len(tables)}):")
63
  for table in tables:
64
  print(f" - {table}")
65
+
66
  # Check if there are any users
67
  user_count = User.query.count()
68
  print(f"\n👥 Current users in database: {user_count}")
69
+
70
  if user_count == 0:
71
  print("\n💡 No users yet. Users will be created when they register via the app.")
72
+
73
  print("\n🎉 Database initialization complete!")
74
  print(" Your app is now ready to use persistent PostgreSQL storage.")
web_app/__init__.py CHANGED
@@ -29,7 +29,7 @@ def create_app(config_class=Config):
29
 
30
  # Initialize CSRF protection
31
  csrf.init_app(app)
32
-
33
  # Exempt API endpoints from CSRF (they use token auth)
34
  @csrf.exempt
35
  def csrf_exempt_api():
 
29
 
30
  # Initialize CSRF protection
31
  csrf.init_app(app)
32
+
33
  # Exempt API endpoints from CSRF (they use token auth)
34
  @csrf.exempt
35
  def csrf_exempt_api():
web_app/api_endpoints.py CHANGED
@@ -19,7 +19,8 @@ def get_current_user_from_token():
19
  if auth_header and auth_header.startswith('Bearer '):
20
  token = auth_header.split(' ')[1]
21
  try:
22
- payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
 
23
  user_id = payload.get('user_id')
24
  if user_id:
25
  return User.query.get(user_id)
@@ -36,21 +37,21 @@ def api_auth_required(f):
36
  @wraps(f)
37
  def decorated(*args, **kwargs):
38
  user = None
39
-
40
  # First try JWT token auth (for mobile)
41
  user = get_current_user_from_token()
42
-
43
  # If no token, fall back to session auth (for web)
44
  if not user and current_user.is_authenticated:
45
  user = current_user
46
-
47
  if not user:
48
  return jsonify({'error': 'Authentication required'}), 401
49
-
50
  # Store user in g for access in the route
51
  g.current_user = user
52
  return f(*args, **kwargs)
53
-
54
  return decorated
55
 
56
 
@@ -141,6 +142,18 @@ def get_all_paths():
141
  path_data['topic'] = up.topic
142
  path_data['created_at'] = up.created_at.isoformat(
143
  ) if up.created_at else None
 
 
 
 
 
 
 
 
 
 
 
 
144
  paths.append(path_data)
145
  return jsonify({'paths': paths}), 200
146
  except Exception as e:
@@ -171,6 +184,17 @@ def get_path_by_id(path_id):
171
  path_data['created_at'] = user_path.created_at.isoformat(
172
  ) if user_path.created_at else None
173
 
 
 
 
 
 
 
 
 
 
 
 
174
  return jsonify(path_data), 200
175
  except Exception as e:
176
  current_app.logger.error(f"Error getting path: {str(e)}")
 
19
  if auth_header and auth_header.startswith('Bearer '):
20
  token = auth_header.split(' ')[1]
21
  try:
22
+ payload = jwt.decode(
23
+ token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
24
  user_id = payload.get('user_id')
25
  if user_id:
26
  return User.query.get(user_id)
 
37
  @wraps(f)
38
  def decorated(*args, **kwargs):
39
  user = None
40
+
41
  # First try JWT token auth (for mobile)
42
  user = get_current_user_from_token()
43
+
44
  # If no token, fall back to session auth (for web)
45
  if not user and current_user.is_authenticated:
46
  user = current_user
47
+
48
  if not user:
49
  return jsonify({'error': 'Authentication required'}), 401
50
+
51
  # Store user in g for access in the route
52
  g.current_user = user
53
  return f(*args, **kwargs)
54
+
55
  return decorated
56
 
57
 
 
142
  path_data['topic'] = up.topic
143
  path_data['created_at'] = up.created_at.isoformat(
144
  ) if up.created_at else None
145
+
146
+ # Include milestone completion progress
147
+ progress_entries = LearningProgress.query.filter_by(
148
+ user_learning_path_id=up.id
149
+ ).all()
150
+ completed_milestones = {}
151
+ for progress in progress_entries:
152
+ completed_milestones[progress.milestone_identifier] = (
153
+ progress.status == 'completed'
154
+ )
155
+ path_data['completedMilestones'] = completed_milestones
156
+
157
  paths.append(path_data)
158
  return jsonify({'paths': paths}), 200
159
  except Exception as e:
 
184
  path_data['created_at'] = user_path.created_at.isoformat(
185
  ) if user_path.created_at else None
186
 
187
+ # Include milestone completion progress
188
+ progress_entries = LearningProgress.query.filter_by(
189
+ user_learning_path_id=path_id
190
+ ).all()
191
+ completed_milestones = {}
192
+ for progress in progress_entries:
193
+ completed_milestones[progress.milestone_identifier] = (
194
+ progress.status == 'completed'
195
+ )
196
+ path_data['completedMilestones'] = completed_milestones
197
+
198
  return jsonify(path_data), 200
199
  except Exception as e:
200
  current_app.logger.error(f"Error getting path: {str(e)}")
web_app/auth_routes.py CHANGED
@@ -36,7 +36,8 @@ def generate_jwt_token(user_id, expires_in_days=30):
36
  def verify_jwt_token(token):
37
  """Verify a JWT token and return the user_id"""
38
  try:
39
- payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
 
40
  return payload.get('user_id')
41
  except jwt.ExpiredSignatureError:
42
  return None
@@ -49,27 +50,27 @@ def token_required(f):
49
  @wraps(f)
50
  def decorated(*args, **kwargs):
51
  token = None
52
-
53
  # Check for token in Authorization header
54
  auth_header = request.headers.get('Authorization')
55
  if auth_header and auth_header.startswith('Bearer '):
56
  token = auth_header.split(' ')[1]
57
-
58
  if not token:
59
  return jsonify({'error': 'Authentication token is missing'}), 401
60
-
61
  user_id = verify_jwt_token(token)
62
  if not user_id:
63
  return jsonify({'error': 'Invalid or expired token'}), 401
64
-
65
  # Get the user
66
  user = User.query.get(user_id)
67
  if not user:
68
  return jsonify({'error': 'User not found'}), 401
69
-
70
  # Pass the user to the route
71
  return f(user, *args, **kwargs)
72
-
73
  return decorated
74
 
75
 
@@ -82,7 +83,8 @@ def register():
82
 
83
  # Handle form submission
84
  if form.validate_on_submit():
85
- logger.info(f"Registration form validated for email: {form.email.data}")
 
86
  user = User(username=form.username.data, email=form.email.data)
87
  user.set_password(form.password.data)
88
 
@@ -108,12 +110,13 @@ def register():
108
  logger.info(f"User logged in after registration: {user.email}")
109
  dashboard_url = url_for('main.dashboard')
110
  logger.info(f"Redirecting to dashboard: {dashboard_url}")
111
-
112
  # Use render template with meta refresh as fallback
113
  return render_template('redirect.html', redirect_url=dashboard_url)
114
  else:
115
  if form.is_submitted():
116
- logger.warning(f"Registration form validation failed: {form.errors}")
 
117
 
118
  return render_template('register.html', title='Join the Community', form=form)
119
 
@@ -132,12 +135,14 @@ def login():
132
  if user is None or not user.check_password(form.password.data):
133
  # Helpful error message
134
  if user is None:
135
- logger.warning(f"Login failed: No user found for email {form.email.data}")
 
136
  flash(
137
  'No account found with this email. Would you like to register?', 'warning')
138
  return redirect(url_for('auth.register', email=form.email.data))
139
  else:
140
- logger.warning(f"Login failed: Wrong password for {form.email.data}")
 
141
  flash('Incorrect password. Please try again.', 'danger')
142
  return redirect(url_for('auth.login'))
143
 
@@ -161,7 +166,7 @@ def login():
161
  next_page = url_for('main.dashboard')
162
 
163
  logger.info(f"Redirecting to: {next_page}")
164
-
165
  # Use render template with meta refresh as fallback
166
  return render_template('redirect.html', redirect_url=next_page)
167
  else:
 
36
  def verify_jwt_token(token):
37
  """Verify a JWT token and return the user_id"""
38
  try:
39
+ payload = jwt.decode(
40
+ token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
41
  return payload.get('user_id')
42
  except jwt.ExpiredSignatureError:
43
  return None
 
50
  @wraps(f)
51
  def decorated(*args, **kwargs):
52
  token = None
53
+
54
  # Check for token in Authorization header
55
  auth_header = request.headers.get('Authorization')
56
  if auth_header and auth_header.startswith('Bearer '):
57
  token = auth_header.split(' ')[1]
58
+
59
  if not token:
60
  return jsonify({'error': 'Authentication token is missing'}), 401
61
+
62
  user_id = verify_jwt_token(token)
63
  if not user_id:
64
  return jsonify({'error': 'Invalid or expired token'}), 401
65
+
66
  # Get the user
67
  user = User.query.get(user_id)
68
  if not user:
69
  return jsonify({'error': 'User not found'}), 401
70
+
71
  # Pass the user to the route
72
  return f(user, *args, **kwargs)
73
+
74
  return decorated
75
 
76
 
 
83
 
84
  # Handle form submission
85
  if form.validate_on_submit():
86
+ logger.info(
87
+ f"Registration form validated for email: {form.email.data}")
88
  user = User(username=form.username.data, email=form.email.data)
89
  user.set_password(form.password.data)
90
 
 
110
  logger.info(f"User logged in after registration: {user.email}")
111
  dashboard_url = url_for('main.dashboard')
112
  logger.info(f"Redirecting to dashboard: {dashboard_url}")
113
+
114
  # Use render template with meta refresh as fallback
115
  return render_template('redirect.html', redirect_url=dashboard_url)
116
  else:
117
  if form.is_submitted():
118
+ logger.warning(
119
+ f"Registration form validation failed: {form.errors}")
120
 
121
  return render_template('register.html', title='Join the Community', form=form)
122
 
 
135
  if user is None or not user.check_password(form.password.data):
136
  # Helpful error message
137
  if user is None:
138
+ logger.warning(
139
+ f"Login failed: No user found for email {form.email.data}")
140
  flash(
141
  'No account found with this email. Would you like to register?', 'warning')
142
  return redirect(url_for('auth.register', email=form.email.data))
143
  else:
144
+ logger.warning(
145
+ f"Login failed: Wrong password for {form.email.data}")
146
  flash('Incorrect password. Please try again.', 'danger')
147
  return redirect(url_for('auth.login'))
148
 
 
166
  next_page = url_for('main.dashboard')
167
 
168
  logger.info(f"Redirecting to: {next_page}")
169
+
170
  # Use render template with meta refresh as fallback
171
  return render_template('redirect.html', redirect_url=next_page)
172
  else:
web_app/models.py CHANGED
@@ -2,80 +2,96 @@ from datetime import datetime
2
  from flask_sqlalchemy import SQLAlchemy
3
  from flask_login import UserMixin
4
  from werkzeug.security import generate_password_hash, check_password_hash
5
- from web_app import db, login_manager # Assuming db and login_manager are initialized in __init__.py
 
6
  from flask_dance.consumer.storage.sqla import OAuthConsumerMixin
7
  import uuid
8
 
 
9
  @login_manager.user_loader
10
  def load_user(user_id):
11
  return User.query.get(int(user_id))
12
 
 
13
  class User(UserMixin, db.Model):
14
  __tablename__ = 'users'
15
-
16
  id = db.Column(db.Integer, primary_key=True)
17
- username = db.Column(db.String(64), index=True, unique=True, nullable=False)
 
18
  email = db.Column(db.String(120), index=True, unique=True, nullable=False)
19
  password_hash = db.Column(db.String(256))
20
  created_at = db.Column(db.DateTime, default=datetime.utcnow)
21
  last_seen = db.Column(db.DateTime, default=datetime.utcnow)
22
- registration_source = db.Column(db.String(20), default='email_password') # 'email_password', 'google', etc.
 
23
  login_count = db.Column(db.Integer, default=0)
24
-
25
  # Profile information (optional)
26
- display_name = db.Column(db.String(100)) # For a more personalized display name vs username
 
27
  bio = db.Column(db.Text)
28
-
29
  # Relationships for Feature 1: User Accounts & Progress Tracking
30
  # A user can have multiple learning paths they've generated or saved
31
- learning_paths = db.relationship('UserLearningPath', backref='author', lazy='dynamic')
 
32
 
33
  def set_password(self, password):
34
  self.password_hash = generate_password_hash(password)
35
-
36
  def check_password(self, password):
37
  return check_password_hash(self.password_hash, password)
38
 
39
  def __repr__(self):
40
  return f'<User {self.username}>'
41
 
 
42
  class UserLearningPath(db.Model):
43
  __tablename__ = 'user_learning_paths'
44
 
45
- id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
 
46
  user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
47
  # Storing the original AI-generated path data as JSON for now
48
  # This can be normalized further if needed for Feature 2 (Enhanced Resource Management)
49
- path_data_json = db.Column(db.JSON, nullable=False)
50
- title = db.Column(db.String(200), nullable=True) # Extracted from path_data for easier display
51
- topic = db.Column(db.String(100), nullable=True) # Extracted from path_data
 
 
52
  created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow)
53
  last_accessed_at = db.Column(db.DateTime, default=datetime.utcnow)
54
  is_archived = db.Column(db.Boolean, default=False)
55
 
56
  # Relationships for Feature 1: Progress Tracking
57
  # A learning path can have multiple progress entries (one per milestone)
58
- progress_entries = db.relationship('LearningProgress', backref='path', lazy='dynamic', cascade='all, delete-orphan')
 
59
 
60
  def __repr__(self):
61
  return f'<UserLearningPath {self.id} for User {self.user_id}>'
62
 
 
63
  class LearningProgress(db.Model):
64
  __tablename__ = 'learning_progress'
65
 
66
  id = db.Column(db.Integer, primary_key=True)
67
- user_learning_path_id = db.Column(db.String(36), db.ForeignKey('user_learning_paths.id'), nullable=False)
 
68
  # Assuming milestones have a unique identifier within the path_data_json
69
  # For simplicity, let's say milestone_title or an index can serve as this ID for now.
70
  # This might need refinement based on how milestones are structured in path_data_json.
71
- milestone_identifier = db.Column(db.String(200), nullable=False)
72
- status = db.Column(db.String(50), default='not_started') # e.g., 'not_started', 'in_progress', 'completed'
 
73
  started_at = db.Column(db.DateTime)
74
  completed_at = db.Column(db.DateTime)
75
  notes = db.Column(db.Text)
76
  # For Feature 3 (Interactive Learning - Quizzes), we might add quiz attempts here or in a separate table
77
 
78
- __table_args__ = (db.UniqueConstraint('user_learning_path_id', 'milestone_identifier', name='_user_path_milestone_uc'),)
 
79
 
80
  def __repr__(self):
81
  return f'<LearningProgress for Milestone {self.milestone_identifier} in Path {self.user_learning_path_id}>'
@@ -90,23 +106,28 @@ class ResourceProgress(db.Model):
90
 
91
  id = db.Column(db.Integer, primary_key=True)
92
  user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
93
- learning_path_id = db.Column(db.String(36), db.ForeignKey('user_learning_paths.id'), nullable=False)
94
- milestone_index = db.Column(db.Integer, nullable=False) # 0-based index of milestone
95
- resource_index = db.Column(db.Integer, nullable=False) # 0-based index of resource within milestone
96
- resource_url = db.Column(db.String(500), nullable=False) # Store URL for reference
97
-
 
 
 
 
98
  # Progress tracking
99
  completed = db.Column(db.Boolean, default=False)
100
  completed_at = db.Column(db.DateTime, nullable=True)
101
-
102
  # Metadata
103
  created_at = db.Column(db.DateTime, default=datetime.utcnow)
104
- updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
 
105
 
106
  # Unique constraint: one entry per user, path, milestone, and resource
107
  __table_args__ = (
108
- db.UniqueConstraint('user_id', 'learning_path_id', 'milestone_index', 'resource_index',
109
- name='_user_path_milestone_resource_uc'),
110
  )
111
 
112
  def __repr__(self):
@@ -122,21 +143,24 @@ class MilestoneProgress(db.Model):
122
 
123
  id = db.Column(db.Integer, primary_key=True)
124
  user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
125
- learning_path_id = db.Column(db.String(36), db.ForeignKey('user_learning_paths.id'), nullable=False)
126
- milestone_index = db.Column(db.Integer, nullable=False) # 0-based index of milestone
127
-
 
 
128
  # Progress tracking
129
  completed = db.Column(db.Boolean, default=False)
130
  completed_at = db.Column(db.DateTime, nullable=True)
131
-
132
  # Metadata
133
  created_at = db.Column(db.DateTime, default=datetime.utcnow)
134
- updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
 
135
 
136
  # Unique constraint: one entry per user, path, and milestone
137
  __table_args__ = (
138
- db.UniqueConstraint('user_id', 'learning_path_id', 'milestone_index',
139
- name='_milestone_progress_uc'),
140
  )
141
 
142
  def __repr__(self):
@@ -182,7 +206,7 @@ class MilestoneProgress(db.Model):
182
  class ChatMessage(db.Model):
183
  """
184
  Stores all conversation messages between user and AI assistant.
185
-
186
  Enhanced with:
187
  - Conversation memory and context
188
  - Multi-turn dialogue support
@@ -191,45 +215,51 @@ class ChatMessage(db.Model):
191
  - Automatic cleanup utilities
192
  """
193
  __tablename__ = 'chat_messages'
194
-
195
  id = db.Column(db.Integer, primary_key=True)
196
  user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
197
- learning_path_id = db.Column(db.String(36), db.ForeignKey('user_learning_paths.id'), nullable=True)
198
-
 
199
  # Message content
200
  message = db.Column(db.Text, nullable=False)
201
  role = db.Column(db.String(20), nullable=False) # 'user' or 'assistant'
202
-
203
  # Conversation grouping (NEW: enhanced from session_id)
204
- conversation_id = db.Column(db.String(36), nullable=True, index=True) # Groups related messages
205
-
 
206
  # Learning path context (NEW)
207
- context = db.Column(db.JSON, nullable=True) # Stores path state, progress, current milestone
208
-
 
209
  # Intent classification (Phase 2)
210
- intent = db.Column(db.String(50), nullable=True) # 'modify_path', 'check_progress', 'ask_question', 'general'
211
- entities = db.Column(db.JSON, nullable=True) # Extracted entities from message
212
-
 
 
213
  # Metadata
214
  timestamp = db.Column(db.DateTime, default=datetime.utcnow, index=True)
215
  tokens_used = db.Column(db.Integer, default=0) # Track API costs
216
- response_time_ms = db.Column(db.Integer, nullable=True) # Performance tracking
217
-
 
218
  # Legacy field (kept for backward compatibility)
219
  session_id = db.Column(db.String(36), nullable=True, index=True)
220
-
221
  def __repr__(self):
222
  return f'<ChatMessage {self.id} by User {self.user_id} ({self.role}) in Conversation {self.conversation_id}>'
223
-
224
  @staticmethod
225
  def get_conversation_history(conversation_id, limit=10):
226
  """
227
  Get recent messages from a conversation.
228
-
229
  Args:
230
  conversation_id: The conversation ID to fetch
231
  limit: Maximum number of messages to return (default: 10)
232
-
233
  Returns:
234
  List of ChatMessage objects, ordered by timestamp (oldest first)
235
  """
@@ -238,17 +268,17 @@ class ChatMessage(db.Model):
238
  ).order_by(
239
  ChatMessage.timestamp.asc()
240
  ).limit(limit).all()
241
-
242
  return messages
243
-
244
  @staticmethod
245
  def get_recent_context(conversation_id):
246
  """
247
  Get the most recent context from a conversation.
248
-
249
  Args:
250
  conversation_id: The conversation ID
251
-
252
  Returns:
253
  Dictionary with learning path context, or None
254
  """
@@ -260,60 +290,61 @@ class ChatMessage(db.Model):
260
  ).order_by(
261
  ChatMessage.timestamp.desc()
262
  ).first()
263
-
264
  return message.context if message else None
265
-
266
  @staticmethod
267
  def clean_old_messages(days=7):
268
  """
269
  Delete messages older than specified days.
270
-
271
  Args:
272
  days: Number of days to keep (default: 7)
273
-
274
  Returns:
275
  Number of messages deleted
276
  """
277
  from datetime import timedelta
278
  cutoff_date = datetime.utcnow() - timedelta(days=days)
279
-
280
  old_messages = ChatMessage.query.filter(
281
  ChatMessage.timestamp < cutoff_date
282
  ).all()
283
-
284
  count = len(old_messages)
285
-
286
  for message in old_messages:
287
  db.session.delete(message)
288
-
289
  db.session.commit()
290
-
291
  return count
292
-
293
  @staticmethod
294
  def get_conversation_stats(conversation_id):
295
  """
296
  Get statistics about a conversation.
297
-
298
  Args:
299
  conversation_id: The conversation ID
300
-
301
  Returns:
302
  Dictionary with conversation statistics
303
  """
304
  messages = ChatMessage.query.filter_by(
305
  conversation_id=conversation_id
306
  ).all()
307
-
308
  if not messages:
309
  return None
310
-
311
  user_messages = [m for m in messages if m.role == 'user']
312
  assistant_messages = [m for m in messages if m.role == 'assistant']
313
-
314
  total_tokens = sum(m.tokens_used for m in messages if m.tokens_used)
315
- avg_response_time = sum(m.response_time_ms for m in assistant_messages if m.response_time_ms) / len(assistant_messages) if assistant_messages else 0
316
-
 
317
  return {
318
  'total_messages': len(messages),
319
  'user_messages': len(user_messages),
@@ -328,7 +359,7 @@ class ChatMessage(db.Model):
328
  class PathModification(db.Model):
329
  """
330
  Tracks all modifications made to learning paths via chatbot.
331
-
332
  This enables:
333
  - Modification history and audit trail
334
  - Undo functionality
@@ -336,25 +367,31 @@ class PathModification(db.Model):
336
  - Path evolution tracking
337
  """
338
  __tablename__ = 'path_modifications'
339
-
340
  id = db.Column(db.Integer, primary_key=True)
341
- learning_path_id = db.Column(db.String(36), db.ForeignKey('user_learning_paths.id'), nullable=False)
 
342
  user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
343
- chat_message_id = db.Column(db.Integer, db.ForeignKey('chat_messages.id'), nullable=True)
344
-
 
345
  # What changed
346
- modification_type = db.Column(db.String(50), nullable=False) # 'add_resource', 'modify_milestone', 'split_milestone', etc.
347
- target_path = db.Column(db.String(200), nullable=True) # JSON path to modified element (e.g., 'milestones[2].resources')
348
-
 
 
349
  # Change details
350
- change_description = db.Column(db.Text, nullable=False) # Human-readable description
 
351
  old_value = db.Column(db.JSON, nullable=True) # Previous value (for undo)
352
  new_value = db.Column(db.JSON, nullable=True) # New value
353
-
354
  # Metadata
355
  timestamp = db.Column(db.DateTime, default=datetime.utcnow, index=True)
356
- is_reverted = db.Column(db.Boolean, default=False) # If user undid this change
357
-
 
358
  def __repr__(self):
359
  return f'<PathModification {self.id} for Path {self.learning_path_id}>'
360
 
@@ -362,7 +399,7 @@ class PathModification(db.Model):
362
  class ConversationSession(db.Model):
363
  """
364
  Groups related chat messages into sessions.
365
-
366
  This enables:
367
  - Session-based context management
368
  - Conversation analytics
@@ -370,26 +407,28 @@ class ConversationSession(db.Model):
370
  - Better context window management
371
  """
372
  __tablename__ = 'conversation_sessions'
373
-
374
- id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
 
375
  user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
376
- learning_path_id = db.Column(db.String(36), db.ForeignKey('user_learning_paths.id'), nullable=True)
377
-
 
378
  # Session metadata
379
  started_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
380
  last_activity_at = db.Column(db.DateTime, default=datetime.utcnow)
381
  ended_at = db.Column(db.DateTime, nullable=True)
382
-
383
  # Session summary (generated by AI)
384
  summary = db.Column(db.Text, nullable=True)
385
-
386
  # Session stats
387
  message_count = db.Column(db.Integer, default=0)
388
  total_tokens_used = db.Column(db.Integer, default=0)
389
-
390
  # Session state
391
  is_active = db.Column(db.Boolean, default=True)
392
-
393
  def __repr__(self):
394
  return f'<ConversationSession {self.id} for User {self.user_id}>'
395
 
@@ -397,6 +436,6 @@ class ConversationSession(db.Model):
397
  class OAuth(OAuthConsumerMixin, db.Model):
398
  """Store OAuth tokens for Flask-Dance"""
399
  __tablename__ = 'flask_dance_oauth'
400
-
401
  user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
402
  user = db.relationship('User', backref='oauth_tokens')
 
2
  from flask_sqlalchemy import SQLAlchemy
3
  from flask_login import UserMixin
4
  from werkzeug.security import generate_password_hash, check_password_hash
5
+ # Assuming db and login_manager are initialized in __init__.py
6
+ from web_app import db, login_manager
7
  from flask_dance.consumer.storage.sqla import OAuthConsumerMixin
8
  import uuid
9
 
10
+
11
  @login_manager.user_loader
12
  def load_user(user_id):
13
  return User.query.get(int(user_id))
14
 
15
+
16
  class User(UserMixin, db.Model):
17
  __tablename__ = 'users'
18
+
19
  id = db.Column(db.Integer, primary_key=True)
20
+ username = db.Column(db.String(64), index=True,
21
+ unique=True, nullable=False)
22
  email = db.Column(db.String(120), index=True, unique=True, nullable=False)
23
  password_hash = db.Column(db.String(256))
24
  created_at = db.Column(db.DateTime, default=datetime.utcnow)
25
  last_seen = db.Column(db.DateTime, default=datetime.utcnow)
26
+ # 'email_password', 'google', etc.
27
+ registration_source = db.Column(db.String(20), default='email_password')
28
  login_count = db.Column(db.Integer, default=0)
29
+
30
  # Profile information (optional)
31
+ # For a more personalized display name vs username
32
+ display_name = db.Column(db.String(100))
33
  bio = db.Column(db.Text)
34
+
35
  # Relationships for Feature 1: User Accounts & Progress Tracking
36
  # A user can have multiple learning paths they've generated or saved
37
+ learning_paths = db.relationship(
38
+ 'UserLearningPath', backref='author', lazy='dynamic')
39
 
40
  def set_password(self, password):
41
  self.password_hash = generate_password_hash(password)
42
+
43
  def check_password(self, password):
44
  return check_password_hash(self.password_hash, password)
45
 
46
  def __repr__(self):
47
  return f'<User {self.username}>'
48
 
49
+
50
  class UserLearningPath(db.Model):
51
  __tablename__ = 'user_learning_paths'
52
 
53
+ id = db.Column(db.String(36), primary_key=True,
54
+ default=lambda: str(uuid.uuid4()))
55
  user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
56
  # Storing the original AI-generated path data as JSON for now
57
  # This can be normalized further if needed for Feature 2 (Enhanced Resource Management)
58
+ path_data_json = db.Column(db.JSON, nullable=False)
59
+ # Extracted from path_data for easier display
60
+ title = db.Column(db.String(200), nullable=True)
61
+ # Extracted from path_data
62
+ topic = db.Column(db.String(100), nullable=True)
63
  created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow)
64
  last_accessed_at = db.Column(db.DateTime, default=datetime.utcnow)
65
  is_archived = db.Column(db.Boolean, default=False)
66
 
67
  # Relationships for Feature 1: Progress Tracking
68
  # A learning path can have multiple progress entries (one per milestone)
69
+ progress_entries = db.relationship(
70
+ 'LearningProgress', backref='path', lazy='dynamic', cascade='all, delete-orphan')
71
 
72
  def __repr__(self):
73
  return f'<UserLearningPath {self.id} for User {self.user_id}>'
74
 
75
+
76
  class LearningProgress(db.Model):
77
  __tablename__ = 'learning_progress'
78
 
79
  id = db.Column(db.Integer, primary_key=True)
80
+ user_learning_path_id = db.Column(db.String(36), db.ForeignKey(
81
+ 'user_learning_paths.id'), nullable=False)
82
  # Assuming milestones have a unique identifier within the path_data_json
83
  # For simplicity, let's say milestone_title or an index can serve as this ID for now.
84
  # This might need refinement based on how milestones are structured in path_data_json.
85
+ milestone_identifier = db.Column(db.String(200), nullable=False)
86
+ # e.g., 'not_started', 'in_progress', 'completed'
87
+ status = db.Column(db.String(50), default='not_started')
88
  started_at = db.Column(db.DateTime)
89
  completed_at = db.Column(db.DateTime)
90
  notes = db.Column(db.Text)
91
  # For Feature 3 (Interactive Learning - Quizzes), we might add quiz attempts here or in a separate table
92
 
93
+ __table_args__ = (db.UniqueConstraint('user_learning_path_id',
94
+ 'milestone_identifier', name='_user_path_milestone_uc'),)
95
 
96
  def __repr__(self):
97
  return f'<LearningProgress for Milestone {self.milestone_identifier} in Path {self.user_learning_path_id}>'
 
106
 
107
  id = db.Column(db.Integer, primary_key=True)
108
  user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
109
+ learning_path_id = db.Column(db.String(36), db.ForeignKey(
110
+ 'user_learning_paths.id'), nullable=False)
111
+ # 0-based index of milestone
112
+ milestone_index = db.Column(db.Integer, nullable=False)
113
+ # 0-based index of resource within milestone
114
+ resource_index = db.Column(db.Integer, nullable=False)
115
+ # Store URL for reference
116
+ resource_url = db.Column(db.String(500), nullable=False)
117
+
118
  # Progress tracking
119
  completed = db.Column(db.Boolean, default=False)
120
  completed_at = db.Column(db.DateTime, nullable=True)
121
+
122
  # Metadata
123
  created_at = db.Column(db.DateTime, default=datetime.utcnow)
124
+ updated_at = db.Column(
125
+ db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
126
 
127
  # Unique constraint: one entry per user, path, milestone, and resource
128
  __table_args__ = (
129
+ db.UniqueConstraint('user_id', 'learning_path_id', 'milestone_index', 'resource_index',
130
+ name='_user_path_milestone_resource_uc'),
131
  )
132
 
133
  def __repr__(self):
 
143
 
144
  id = db.Column(db.Integer, primary_key=True)
145
  user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
146
+ learning_path_id = db.Column(db.String(36), db.ForeignKey(
147
+ 'user_learning_paths.id'), nullable=False)
148
+ # 0-based index of milestone
149
+ milestone_index = db.Column(db.Integer, nullable=False)
150
+
151
  # Progress tracking
152
  completed = db.Column(db.Boolean, default=False)
153
  completed_at = db.Column(db.DateTime, nullable=True)
154
+
155
  # Metadata
156
  created_at = db.Column(db.DateTime, default=datetime.utcnow)
157
+ updated_at = db.Column(
158
+ db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
159
 
160
  # Unique constraint: one entry per user, path, and milestone
161
  __table_args__ = (
162
+ db.UniqueConstraint('user_id', 'learning_path_id', 'milestone_index',
163
+ name='_milestone_progress_uc'),
164
  )
165
 
166
  def __repr__(self):
 
206
  class ChatMessage(db.Model):
207
  """
208
  Stores all conversation messages between user and AI assistant.
209
+
210
  Enhanced with:
211
  - Conversation memory and context
212
  - Multi-turn dialogue support
 
215
  - Automatic cleanup utilities
216
  """
217
  __tablename__ = 'chat_messages'
218
+
219
  id = db.Column(db.Integer, primary_key=True)
220
  user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
221
+ learning_path_id = db.Column(db.String(36), db.ForeignKey(
222
+ 'user_learning_paths.id'), nullable=True)
223
+
224
  # Message content
225
  message = db.Column(db.Text, nullable=False)
226
  role = db.Column(db.String(20), nullable=False) # 'user' or 'assistant'
227
+
228
  # Conversation grouping (NEW: enhanced from session_id)
229
+ # Groups related messages
230
+ conversation_id = db.Column(db.String(36), nullable=True, index=True)
231
+
232
  # Learning path context (NEW)
233
+ # Stores path state, progress, current milestone
234
+ context = db.Column(db.JSON, nullable=True)
235
+
236
  # Intent classification (Phase 2)
237
+ # 'modify_path', 'check_progress', 'ask_question', 'general'
238
+ intent = db.Column(db.String(50), nullable=True)
239
+ # Extracted entities from message
240
+ entities = db.Column(db.JSON, nullable=True)
241
+
242
  # Metadata
243
  timestamp = db.Column(db.DateTime, default=datetime.utcnow, index=True)
244
  tokens_used = db.Column(db.Integer, default=0) # Track API costs
245
+ response_time_ms = db.Column(
246
+ db.Integer, nullable=True) # Performance tracking
247
+
248
  # Legacy field (kept for backward compatibility)
249
  session_id = db.Column(db.String(36), nullable=True, index=True)
250
+
251
  def __repr__(self):
252
  return f'<ChatMessage {self.id} by User {self.user_id} ({self.role}) in Conversation {self.conversation_id}>'
253
+
254
  @staticmethod
255
  def get_conversation_history(conversation_id, limit=10):
256
  """
257
  Get recent messages from a conversation.
258
+
259
  Args:
260
  conversation_id: The conversation ID to fetch
261
  limit: Maximum number of messages to return (default: 10)
262
+
263
  Returns:
264
  List of ChatMessage objects, ordered by timestamp (oldest first)
265
  """
 
268
  ).order_by(
269
  ChatMessage.timestamp.asc()
270
  ).limit(limit).all()
271
+
272
  return messages
273
+
274
  @staticmethod
275
  def get_recent_context(conversation_id):
276
  """
277
  Get the most recent context from a conversation.
278
+
279
  Args:
280
  conversation_id: The conversation ID
281
+
282
  Returns:
283
  Dictionary with learning path context, or None
284
  """
 
290
  ).order_by(
291
  ChatMessage.timestamp.desc()
292
  ).first()
293
+
294
  return message.context if message else None
295
+
296
  @staticmethod
297
  def clean_old_messages(days=7):
298
  """
299
  Delete messages older than specified days.
300
+
301
  Args:
302
  days: Number of days to keep (default: 7)
303
+
304
  Returns:
305
  Number of messages deleted
306
  """
307
  from datetime import timedelta
308
  cutoff_date = datetime.utcnow() - timedelta(days=days)
309
+
310
  old_messages = ChatMessage.query.filter(
311
  ChatMessage.timestamp < cutoff_date
312
  ).all()
313
+
314
  count = len(old_messages)
315
+
316
  for message in old_messages:
317
  db.session.delete(message)
318
+
319
  db.session.commit()
320
+
321
  return count
322
+
323
  @staticmethod
324
  def get_conversation_stats(conversation_id):
325
  """
326
  Get statistics about a conversation.
327
+
328
  Args:
329
  conversation_id: The conversation ID
330
+
331
  Returns:
332
  Dictionary with conversation statistics
333
  """
334
  messages = ChatMessage.query.filter_by(
335
  conversation_id=conversation_id
336
  ).all()
337
+
338
  if not messages:
339
  return None
340
+
341
  user_messages = [m for m in messages if m.role == 'user']
342
  assistant_messages = [m for m in messages if m.role == 'assistant']
343
+
344
  total_tokens = sum(m.tokens_used for m in messages if m.tokens_used)
345
+ avg_response_time = sum(m.response_time_ms for m in assistant_messages if m.response_time_ms) / \
346
+ len(assistant_messages) if assistant_messages else 0
347
+
348
  return {
349
  'total_messages': len(messages),
350
  'user_messages': len(user_messages),
 
359
  class PathModification(db.Model):
360
  """
361
  Tracks all modifications made to learning paths via chatbot.
362
+
363
  This enables:
364
  - Modification history and audit trail
365
  - Undo functionality
 
367
  - Path evolution tracking
368
  """
369
  __tablename__ = 'path_modifications'
370
+
371
  id = db.Column(db.Integer, primary_key=True)
372
+ learning_path_id = db.Column(db.String(36), db.ForeignKey(
373
+ 'user_learning_paths.id'), nullable=False)
374
  user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
375
+ chat_message_id = db.Column(db.Integer, db.ForeignKey(
376
+ 'chat_messages.id'), nullable=True)
377
+
378
  # What changed
379
+ # 'add_resource', 'modify_milestone', 'split_milestone', etc.
380
+ modification_type = db.Column(db.String(50), nullable=False)
381
+ # JSON path to modified element (e.g., 'milestones[2].resources')
382
+ target_path = db.Column(db.String(200), nullable=True)
383
+
384
  # Change details
385
+ # Human-readable description
386
+ change_description = db.Column(db.Text, nullable=False)
387
  old_value = db.Column(db.JSON, nullable=True) # Previous value (for undo)
388
  new_value = db.Column(db.JSON, nullable=True) # New value
389
+
390
  # Metadata
391
  timestamp = db.Column(db.DateTime, default=datetime.utcnow, index=True)
392
+ # If user undid this change
393
+ is_reverted = db.Column(db.Boolean, default=False)
394
+
395
  def __repr__(self):
396
  return f'<PathModification {self.id} for Path {self.learning_path_id}>'
397
 
 
399
  class ConversationSession(db.Model):
400
  """
401
  Groups related chat messages into sessions.
402
+
403
  This enables:
404
  - Session-based context management
405
  - Conversation analytics
 
407
  - Better context window management
408
  """
409
  __tablename__ = 'conversation_sessions'
410
+
411
+ id = db.Column(db.String(36), primary_key=True,
412
+ default=lambda: str(uuid.uuid4()))
413
  user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
414
+ learning_path_id = db.Column(db.String(36), db.ForeignKey(
415
+ 'user_learning_paths.id'), nullable=True)
416
+
417
  # Session metadata
418
  started_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
419
  last_activity_at = db.Column(db.DateTime, default=datetime.utcnow)
420
  ended_at = db.Column(db.DateTime, nullable=True)
421
+
422
  # Session summary (generated by AI)
423
  summary = db.Column(db.Text, nullable=True)
424
+
425
  # Session stats
426
  message_count = db.Column(db.Integer, default=0)
427
  total_tokens_used = db.Column(db.Integer, default=0)
428
+
429
  # Session state
430
  is_active = db.Column(db.Boolean, default=True)
431
+
432
  def __repr__(self):
433
  return f'<ConversationSession {self.id} for User {self.user_id}>'
434
 
 
436
  class OAuth(OAuthConsumerMixin, db.Model):
437
  """Store OAuth tokens for Flask-Dance"""
438
  __tablename__ = 'flask_dance_oauth'
439
+
440
  user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
441
  user = db.relationship('User', backref='oauth_tokens')
web_app/templates/dashboard.html CHANGED
@@ -1,5 +1,6 @@
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">
@@ -7,6 +8,7 @@
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link rel="stylesheet" href="{{ url_for('static', filename='css/glassmorphic.css') }}">
9
  </head>
 
10
  <body class="bg-primary min-h-screen">
11
  <!-- Navigation -->
12
  <nav class="glass-nav py-4 px-6">
@@ -26,13 +28,14 @@
26
  </svg>
27
  </button>
28
  <div class="absolute right-0 mt-2 w-48 glass-card p-2 hidden group-hover:block">
29
- <a href="{{ url_for('auth.logout') }}" class="block px-4 py-2 text-sm text-secondary hover:text-neon-cyan">Logout</a>
 
30
  </div>
31
  </div>
32
  </div>
33
  </div>
34
  </nav>
35
-
36
  <!-- Header -->
37
  <section class="bg-secondary py-12 px-6">
38
  <div class="container mx-auto">
@@ -64,50 +67,55 @@
64
 
65
  <!-- My Learning Paths -->
66
  <h2 class="text-3xl font-bold text-white mb-8">My Learning Paths</h2>
67
-
68
  {% if user_paths %}
69
- <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
70
- {% for path in user_paths %}
71
- <div id="path-{{ path.id }}" class="glass-card p-6 path-card">
72
- <div class="flex items-center gap-2 mb-3">
73
- <span class="px-3 py-1 bg-neon-cyan bg-opacity-20 border border-neon-cyan text-neon-cyan rounded-full text-xs font-medium">{{ path.topic }}</span>
74
- <span class="px-3 py-1 bg-neon-purple bg-opacity-20 border border-neon-purple text-neon-purple rounded-full text-xs font-medium">{{ path.expertise_level|default('Beginner')|title }}</span>
75
- </div>
76
- <h3 class="text-xl font-bold mb-2 text-white">{{ path.title }}</h3>
77
- <p class="text-muted text-sm mb-4">Created: {{ path.created_at }}</p>
78
-
79
- <!-- Progress bar -->
80
- <div class="mb-6">
81
- <div class="flex justify-between text-sm mb-2">
82
- <span class="text-secondary">Progress</span>
83
- <span class="text-neon-cyan font-mono">{{ path.progress_percentage }}%</span>
84
- </div>
85
- <div class="progress-bar-container">
86
- <div class="progress-bar" style="width: {{ path.progress_percentage }}%"></div>
87
- </div>
88
- </div>
89
-
90
- <div class="flex flex-col gap-2">
91
- <a href="{{ url_for('main.view_path', path_id=path.id) }}" class="neon-btn text-center">Continue</a>
92
- <div class="flex gap-2">
93
- <button onclick="archivePath('{{ path.id }}')" class="neon-btn-sm-purple flex-1">
94
- {% if path.is_archived %}Unarchive{% else %}Archive{% endif %}
95
- </button>
96
- <button onclick="deletePath('{{ path.id }}')" class="neon-btn-sm flex-1" style="border-color: var(--status-error); color: var(--status-error);">
97
- Delete
98
- </button>
99
- </div>
100
- </div>
101
  </div>
102
- {% endfor %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  </div>
 
 
104
  {% else %}
105
- <div class="glass-card p-12 text-center">
106
- <div class="text-8xl mb-6">📚</div>
107
- <h3 class="text-3xl font-bold text-white mb-4">No Learning Paths Found</h3>
108
- <p class="text-secondary mb-8">Create your first personalized learning journey!</p>
109
- <a href="/new-path#path-form" class="neon-btn text-lg">Create Your First Path</a>
110
- </div>
111
  {% endif %}
112
  </div>
113
 
@@ -123,35 +131,35 @@
123
  path_id: pathId
124
  })
125
  })
126
- .then(response => response.json())
127
- .then(data => {
128
- if (data.success) {
129
- // Reload the page to reflect changes
130
- window.location.reload();
131
- } else {
132
- alert('Error: ' + data.message);
133
- }
134
- })
135
- .catch(error => {
136
- console.error('Error:', error);
137
- alert('An error occurred. Please try again.');
138
- });
139
  }
140
  </script>
141
-
142
  <!-- JavaScript for path management -->
143
  <script>
144
- document.addEventListener('DOMContentLoaded', function() {
145
  // Handle archive/unarchive buttons
146
  const archiveButtons = document.querySelectorAll('.archive-btn');
147
  const unarchiveButtons = document.querySelectorAll('.unarchive-btn');
148
-
149
  // Archive path
150
  archiveButtons.forEach(btn => {
151
- btn.addEventListener('click', function() {
152
  const pathId = this.getAttribute('data-path-id');
153
  const pathCard = document.getElementById('path-' + pathId);
154
-
155
  // Send request to server
156
  fetch('/archive_path', {
157
  method: 'POST',
@@ -163,19 +171,19 @@
163
  archive: true
164
  })
165
  })
166
- .then(response => response.json())
167
- .then(data => {
168
- if (data.status === 'success') {
169
- // Remove path card with animation
170
- pathCard.classList.add('opacity-0');
171
- setTimeout(() => {
172
- pathCard.remove();
173
-
174
- // Check if there are no more active paths
175
- const remainingPaths = document.querySelectorAll('.path-card');
176
- if (remainingPaths.length === 0) {
177
- const pathsContainer = document.getElementById('active-paths-container');
178
- pathsContainer.innerHTML = `
179
  <div class="bg-white rounded-xl shadow-md p-8 text-center">
180
  <img src="https://img.freepik.com/free-vector/empty-concept-illustration_114360-1188.jpg" alt="No paths found" class="w-64 h-64 mx-auto mb-6">
181
  <h3 class="text-2xl font-bold text-gray-800 mb-4">No Active Learning Paths</h3>
@@ -183,22 +191,22 @@
183
  <a href="/" class="inline-block bg-magenta text-white px-6 py-3 rounded-full font-bold hover:bg-magentaLight transition-colors duration-300">Create Your First Path</a>
184
  </div>
185
  `;
186
- }
187
-
188
- // Update archived paths section
189
- const archivedPathsContainer = document.getElementById('archived-paths-container');
190
- const archivedPathsCount = document.getElementById('archived-paths-count');
191
-
192
- // Create new archived path card
193
- const pathTitle = this.getAttribute('data-path-title');
194
- const pathTopic = this.getAttribute('data-path-topic');
195
- const pathExpertise = this.getAttribute('data-path-expertise');
196
- const pathCreated = this.getAttribute('data-path-created');
197
-
198
- const newArchivedPath = document.createElement('div');
199
- newArchivedPath.id = 'archived-' + pathId;
200
- newArchivedPath.className = 'bg-white rounded-xl shadow-md overflow-hidden archived-path-card';
201
- newArchivedPath.innerHTML = `
202
  <div class="p-6">
203
  <div class="flex items-center gap-2 mb-2">
204
  <span class="px-3 py-1 bg-magentaLight text-white rounded-full text-xs font-medium">${pathTopic}</span>
@@ -212,42 +220,42 @@
212
  </div>
213
  </div>
214
  `;
215
-
216
- // Show archived paths section if it was hidden
217
- const archivedSection = document.getElementById('archived-section');
218
- if (archivedSection.classList.contains('hidden')) {
219
- archivedSection.classList.remove('hidden');
220
- }
221
-
222
- // Add the new archived path
223
- archivedPathsContainer.appendChild(newArchivedPath);
224
-
225
- // Update count
226
- const currentCount = parseInt(archivedPathsCount.textContent) || 0;
227
- archivedPathsCount.textContent = currentCount + 1;
228
-
229
- // Add event listener to the new unarchive button
230
- const newUnarchiveBtn = newArchivedPath.querySelector('.unarchive-btn');
231
- addUnarchiveListener(newUnarchiveBtn);
232
- }, 300);
233
- } else {
234
- console.error('Error archiving path:', data.message);
235
- alert('Error archiving path. Please try again.');
236
- }
237
- })
238
- .catch(error => {
239
- console.error('Error:', error);
240
- alert('An error occurred. Please try again.');
241
- });
242
  });
243
  });
244
-
245
  // Function to add unarchive event listener
246
  function addUnarchiveListener(btn) {
247
- btn.addEventListener('click', function() {
248
  const pathId = this.getAttribute('data-path-id');
249
  const archivedCard = document.getElementById('archived-' + pathId);
250
-
251
  // Send request to server
252
  fetch('/archive_path', {
253
  method: 'POST',
@@ -259,36 +267,36 @@
259
  archive: false
260
  })
261
  })
262
- .then(response => response.json())
263
- .then(data => {
264
- if (data.status === 'success') {
265
- // Remove archived card with animation
266
- archivedCard.classList.add('opacity-0');
267
- setTimeout(() => {
268
- archivedCard.remove();
269
-
270
- // Check if there are no more archived paths
271
- const remainingArchived = document.querySelectorAll('.archived-path-card');
272
- if (remainingArchived.length === 0) {
273
- const archivedSection = document.getElementById('archived-section');
274
- archivedSection.classList.add('hidden');
275
- }
276
-
277
- // Update archived paths count
278
- const archivedPathsCount = document.getElementById('archived-paths-count');
279
- const currentCount = parseInt(archivedPathsCount.textContent) || 0;
280
- archivedPathsCount.textContent = Math.max(0, currentCount - 1);
281
-
282
- // Create new active path card
283
- const pathTitle = this.getAttribute('data-path-title');
284
- const pathTopic = this.getAttribute('data-path-topic');
285
- const pathExpertise = this.getAttribute('data-path-expertise');
286
- const pathCreated = this.getAttribute('data-path-created');
287
-
288
- const newActivePath = document.createElement('div');
289
- newActivePath.id = 'path-' + pathId;
290
- newActivePath.className = 'bg-white rounded-xl shadow-md overflow-hidden path-card opacity-0';
291
- newActivePath.innerHTML = `
292
  <div class="p-6">
293
  <div class="flex items-center gap-2 mb-2">
294
  <span class="px-3 py-1 bg-magentaLight text-white rounded-full text-xs font-medium">${pathTopic}</span>
@@ -309,38 +317,38 @@
309
  </div>
310
  </div>
311
  `;
312
-
313
- // Check if there are no active paths and remove placeholder if needed
314
- const activePathsContainer = document.getElementById('active-paths-container');
315
- const noPathsPlaceholder = activePathsContainer.querySelector('.text-center');
316
- if (noPathsPlaceholder) {
317
- activePathsContainer.innerHTML = '';
318
- }
319
-
320
- // Add the new active path
321
- activePathsContainer.appendChild(newActivePath);
322
-
323
- // Fade in the new card
324
- setTimeout(() => {
325
- newActivePath.classList.remove('opacity-0');
326
- }, 10);
327
-
328
- // Add event listener to the new archive button
329
- const newArchiveBtn = newActivePath.querySelector('.archive-btn');
330
- newArchiveBtn.addEventListener('click', archiveButtons[0].onclick);
331
- }, 300);
332
- } else {
333
- console.error('Error unarchiving path:', data.message);
334
- alert('Error unarchiving path. Please try again.');
335
- }
336
- })
337
- .catch(error => {
338
- console.error('Error:', error);
339
- alert('An error occurred. Please try again.');
340
- });
341
  });
342
  }
343
-
344
  // Add listeners to all unarchive buttons
345
  unarchiveButtons.forEach(btn => {
346
  addUnarchiveListener(btn);
@@ -360,27 +368,28 @@
360
  },
361
  body: JSON.stringify({ path_id: pathId })
362
  })
363
- .then(response => response.json())
364
- .then(data => {
365
- if (data.status === 'success') {
366
- // Remove card from UI
367
- const card = document.getElementById('path-' + pathId);
368
- if (card) card.remove();
369
- // If no cards left, show placeholder
370
- const remaining = document.querySelectorAll('.path-card');
371
- if (remaining.length === 0) {
372
- window.location.reload();
 
 
 
373
  }
374
- } else {
375
- alert('Error: ' + data.message);
376
- }
377
- })
378
- .catch(err => {
379
- console.error('Error deleting path:', err);
380
- alert('An error occurred. Please try again.');
381
- });
382
  }
383
  </script>
384
  <script src="{{ url_for('static', filename='js/theme.js') }}"></script>
385
  </body>
386
- </html>
 
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
8
  <script src="https://cdn.tailwindcss.com"></script>
9
  <link rel="stylesheet" href="{{ url_for('static', filename='css/glassmorphic.css') }}">
10
  </head>
11
+
12
  <body class="bg-primary min-h-screen">
13
  <!-- Navigation -->
14
  <nav class="glass-nav py-4 px-6">
 
28
  </svg>
29
  </button>
30
  <div class="absolute right-0 mt-2 w-48 glass-card p-2 hidden group-hover:block">
31
+ <a href="{{ url_for('auth.logout') }}"
32
+ class="block px-4 py-2 text-sm text-secondary hover:text-neon-cyan">Logout</a>
33
  </div>
34
  </div>
35
  </div>
36
  </div>
37
  </nav>
38
+
39
  <!-- Header -->
40
  <section class="bg-secondary py-12 px-6">
41
  <div class="container mx-auto">
 
67
 
68
  <!-- My Learning Paths -->
69
  <h2 class="text-3xl font-bold text-white mb-8">My Learning Paths</h2>
70
+
71
  {% if user_paths %}
72
+ <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
73
+ {% for path in user_paths %}
74
+ <div id="path-{{ path.id }}" class="glass-card p-6 path-card">
75
+ <div class="flex items-center gap-2 mb-3">
76
+ <span
77
+ class="px-3 py-1 bg-neon-cyan bg-opacity-20 border border-neon-cyan text-neon-cyan rounded-full text-xs font-medium">{{
78
+ path.topic }}</span>
79
+ <span
80
+ class="px-3 py-1 bg-neon-purple bg-opacity-20 border border-neon-purple text-neon-purple rounded-full text-xs font-medium">{{
81
+ path.expertise_level|default('Beginner')|title }}</span>
82
+ </div>
83
+ <h3 class="text-xl font-bold mb-2 text-white">{{ path.title }}</h3>
84
+ <p class="text-muted text-sm mb-4">Created: {{ path.created_at }}</p>
85
+
86
+ <!-- Progress bar -->
87
+ <div class="mb-6">
88
+ <div class="flex justify-between text-sm mb-2">
89
+ <span class="text-secondary">Progress</span>
90
+ <span class="text-neon-cyan font-mono">{{ path.progress_percentage }}%</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  </div>
92
+ <div class="progress-bar-container">
93
+ <div class="progress-bar" style="width: {{ path.progress_percentage }}%"></div>
94
+ </div>
95
+ </div>
96
+
97
+ <div class="flex flex-col gap-2">
98
+ <a href="{{ url_for('main.view_path', path_id=path.id) }}" class="neon-btn text-center">Continue</a>
99
+ <div class="flex gap-2">
100
+ <button onclick="archivePath('{{ path.id }}')" class="neon-btn-sm-purple flex-1">
101
+ {% if path.is_archived %}Unarchive{% else %}Archive{% endif %}
102
+ </button>
103
+ <button onclick="deletePath('{{ path.id }}')" class="neon-btn-sm flex-1"
104
+ style="border-color: var(--status-error); color: var(--status-error);">
105
+ Delete
106
+ </button>
107
+ </div>
108
+ </div>
109
  </div>
110
+ {% endfor %}
111
+ </div>
112
  {% else %}
113
+ <div class="glass-card p-12 text-center">
114
+ <div class="text-8xl mb-6">📚</div>
115
+ <h3 class="text-3xl font-bold text-white mb-4">No Learning Paths Found</h3>
116
+ <p class="text-secondary mb-8">Create your first personalized learning journey!</p>
117
+ <a href="/new-path#path-form" class="neon-btn text-lg">Create Your First Path</a>
118
+ </div>
119
  {% endif %}
120
  </div>
121
 
 
131
  path_id: pathId
132
  })
133
  })
134
+ .then(response => response.json())
135
+ .then(data => {
136
+ if (data.success) {
137
+ // Reload the page to reflect changes
138
+ window.location.reload();
139
+ } else {
140
+ alert('Error: ' + data.message);
141
+ }
142
+ })
143
+ .catch(error => {
144
+ console.error('Error:', error);
145
+ alert('An error occurred. Please try again.');
146
+ });
147
  }
148
  </script>
149
+
150
  <!-- JavaScript for path management -->
151
  <script>
152
+ document.addEventListener('DOMContentLoaded', function () {
153
  // Handle archive/unarchive buttons
154
  const archiveButtons = document.querySelectorAll('.archive-btn');
155
  const unarchiveButtons = document.querySelectorAll('.unarchive-btn');
156
+
157
  // Archive path
158
  archiveButtons.forEach(btn => {
159
+ btn.addEventListener('click', function () {
160
  const pathId = this.getAttribute('data-path-id');
161
  const pathCard = document.getElementById('path-' + pathId);
162
+
163
  // Send request to server
164
  fetch('/archive_path', {
165
  method: 'POST',
 
171
  archive: true
172
  })
173
  })
174
+ .then(response => response.json())
175
+ .then(data => {
176
+ if (data.status === 'success') {
177
+ // Remove path card with animation
178
+ pathCard.classList.add('opacity-0');
179
+ setTimeout(() => {
180
+ pathCard.remove();
181
+
182
+ // Check if there are no more active paths
183
+ const remainingPaths = document.querySelectorAll('.path-card');
184
+ if (remainingPaths.length === 0) {
185
+ const pathsContainer = document.getElementById('active-paths-container');
186
+ pathsContainer.innerHTML = `
187
  <div class="bg-white rounded-xl shadow-md p-8 text-center">
188
  <img src="https://img.freepik.com/free-vector/empty-concept-illustration_114360-1188.jpg" alt="No paths found" class="w-64 h-64 mx-auto mb-6">
189
  <h3 class="text-2xl font-bold text-gray-800 mb-4">No Active Learning Paths</h3>
 
191
  <a href="/" class="inline-block bg-magenta text-white px-6 py-3 rounded-full font-bold hover:bg-magentaLight transition-colors duration-300">Create Your First Path</a>
192
  </div>
193
  `;
194
+ }
195
+
196
+ // Update archived paths section
197
+ const archivedPathsContainer = document.getElementById('archived-paths-container');
198
+ const archivedPathsCount = document.getElementById('archived-paths-count');
199
+
200
+ // Create new archived path card
201
+ const pathTitle = this.getAttribute('data-path-title');
202
+ const pathTopic = this.getAttribute('data-path-topic');
203
+ const pathExpertise = this.getAttribute('data-path-expertise');
204
+ const pathCreated = this.getAttribute('data-path-created');
205
+
206
+ const newArchivedPath = document.createElement('div');
207
+ newArchivedPath.id = 'archived-' + pathId;
208
+ newArchivedPath.className = 'bg-white rounded-xl shadow-md overflow-hidden archived-path-card';
209
+ newArchivedPath.innerHTML = `
210
  <div class="p-6">
211
  <div class="flex items-center gap-2 mb-2">
212
  <span class="px-3 py-1 bg-magentaLight text-white rounded-full text-xs font-medium">${pathTopic}</span>
 
220
  </div>
221
  </div>
222
  `;
223
+
224
+ // Show archived paths section if it was hidden
225
+ const archivedSection = document.getElementById('archived-section');
226
+ if (archivedSection.classList.contains('hidden')) {
227
+ archivedSection.classList.remove('hidden');
228
+ }
229
+
230
+ // Add the new archived path
231
+ archivedPathsContainer.appendChild(newArchivedPath);
232
+
233
+ // Update count
234
+ const currentCount = parseInt(archivedPathsCount.textContent) || 0;
235
+ archivedPathsCount.textContent = currentCount + 1;
236
+
237
+ // Add event listener to the new unarchive button
238
+ const newUnarchiveBtn = newArchivedPath.querySelector('.unarchive-btn');
239
+ addUnarchiveListener(newUnarchiveBtn);
240
+ }, 300);
241
+ } else {
242
+ console.error('Error archiving path:', data.message);
243
+ alert('Error archiving path. Please try again.');
244
+ }
245
+ })
246
+ .catch(error => {
247
+ console.error('Error:', error);
248
+ alert('An error occurred. Please try again.');
249
+ });
250
  });
251
  });
252
+
253
  // Function to add unarchive event listener
254
  function addUnarchiveListener(btn) {
255
+ btn.addEventListener('click', function () {
256
  const pathId = this.getAttribute('data-path-id');
257
  const archivedCard = document.getElementById('archived-' + pathId);
258
+
259
  // Send request to server
260
  fetch('/archive_path', {
261
  method: 'POST',
 
267
  archive: false
268
  })
269
  })
270
+ .then(response => response.json())
271
+ .then(data => {
272
+ if (data.status === 'success') {
273
+ // Remove archived card with animation
274
+ archivedCard.classList.add('opacity-0');
275
+ setTimeout(() => {
276
+ archivedCard.remove();
277
+
278
+ // Check if there are no more archived paths
279
+ const remainingArchived = document.querySelectorAll('.archived-path-card');
280
+ if (remainingArchived.length === 0) {
281
+ const archivedSection = document.getElementById('archived-section');
282
+ archivedSection.classList.add('hidden');
283
+ }
284
+
285
+ // Update archived paths count
286
+ const archivedPathsCount = document.getElementById('archived-paths-count');
287
+ const currentCount = parseInt(archivedPathsCount.textContent) || 0;
288
+ archivedPathsCount.textContent = Math.max(0, currentCount - 1);
289
+
290
+ // Create new active path card
291
+ const pathTitle = this.getAttribute('data-path-title');
292
+ const pathTopic = this.getAttribute('data-path-topic');
293
+ const pathExpertise = this.getAttribute('data-path-expertise');
294
+ const pathCreated = this.getAttribute('data-path-created');
295
+
296
+ const newActivePath = document.createElement('div');
297
+ newActivePath.id = 'path-' + pathId;
298
+ newActivePath.className = 'bg-white rounded-xl shadow-md overflow-hidden path-card opacity-0';
299
+ newActivePath.innerHTML = `
300
  <div class="p-6">
301
  <div class="flex items-center gap-2 mb-2">
302
  <span class="px-3 py-1 bg-magentaLight text-white rounded-full text-xs font-medium">${pathTopic}</span>
 
317
  </div>
318
  </div>
319
  `;
320
+
321
+ // Check if there are no active paths and remove placeholder if needed
322
+ const activePathsContainer = document.getElementById('active-paths-container');
323
+ const noPathsPlaceholder = activePathsContainer.querySelector('.text-center');
324
+ if (noPathsPlaceholder) {
325
+ activePathsContainer.innerHTML = '';
326
+ }
327
+
328
+ // Add the new active path
329
+ activePathsContainer.appendChild(newActivePath);
330
+
331
+ // Fade in the new card
332
+ setTimeout(() => {
333
+ newActivePath.classList.remove('opacity-0');
334
+ }, 10);
335
+
336
+ // Add event listener to the new archive button
337
+ const newArchiveBtn = newActivePath.querySelector('.archive-btn');
338
+ newArchiveBtn.addEventListener('click', archiveButtons[0].onclick);
339
+ }, 300);
340
+ } else {
341
+ console.error('Error unarchiving path:', data.message);
342
+ alert('Error unarchiving path. Please try again.');
343
+ }
344
+ })
345
+ .catch(error => {
346
+ console.error('Error:', error);
347
+ alert('An error occurred. Please try again.');
348
+ });
349
  });
350
  }
351
+
352
  // Add listeners to all unarchive buttons
353
  unarchiveButtons.forEach(btn => {
354
  addUnarchiveListener(btn);
 
368
  },
369
  body: JSON.stringify({ path_id: pathId })
370
  })
371
+ .then(response => response.json())
372
+ .then(data => {
373
+ if (data.status === 'success') {
374
+ // Remove card from UI
375
+ const card = document.getElementById('path-' + pathId);
376
+ if (card) card.remove();
377
+ // If no cards left, show placeholder
378
+ const remaining = document.querySelectorAll('.path-card');
379
+ if (remaining.length === 0) {
380
+ window.location.reload();
381
+ }
382
+ } else {
383
+ alert('Error: ' + data.message);
384
  }
385
+ })
386
+ .catch(err => {
387
+ console.error('Error deleting path:', err);
388
+ alert('An error occurred. Please try again.');
389
+ });
 
 
 
390
  }
391
  </script>
392
  <script src="{{ url_for('static', filename='js/theme.js') }}"></script>
393
  </body>
394
+
395
+ </html>
web_app/templates/index.html CHANGED
@@ -1,12 +1,13 @@
1
  <!DOCTYPE html>
2
  <html lang="en" class="dark">
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>AI Learning Path Generator</title>
7
  <!-- Initialize theme immediately to prevent flash -->
8
  <script>
9
- (function() {
10
  const theme = localStorage.getItem('theme');
11
  if (theme === 'light') {
12
  document.documentElement.classList.remove('dark');
@@ -32,24 +33,25 @@
32
  font-weight: 600;
33
  box-shadow: 0 0 0 2px var(--tw-ring-color, currentColor);
34
  }
35
-
36
- .bg-magenta, .magenta-bg {
 
37
  background-color: #ff50c5 !important;
38
  }
39
-
40
  .text-magenta {
41
  color: #ff50c5 !important;
42
  }
43
-
44
  .yellow-bg {
45
  background-color: #F9C846;
46
  }
47
-
48
  /* Ensure footer uses proper background */
49
  footer {
50
  background-color: rgba(31, 41, 55, 0.95) !important;
51
  }
52
-
53
  /* Category Accordion Styles */
54
  .category-accordion {
55
  border: 1px solid var(--glass-border);
@@ -57,57 +59,57 @@
57
  overflow: hidden;
58
  transition: all 0.3s ease;
59
  }
60
-
61
  .category-accordion:hover {
62
  border-color: rgba(255, 80, 197, 0.4);
63
  }
64
-
65
  .category-header {
66
  cursor: pointer;
67
  user-select: none;
68
  }
69
-
70
  .category-header:active {
71
  transform: scale(0.98);
72
  }
73
-
74
  .category-accordion.active .category-arrow {
75
  transform: rotate(180deg);
76
  }
77
-
78
  .category-content {
79
  transition: max-height 0.4s ease-out, opacity 0.3s ease-out;
80
  overflow: hidden;
81
  }
82
-
83
  .category-content.hidden {
84
  max-height: 0 !important;
85
  opacity: 0;
86
  }
87
-
88
  .category-content.show {
89
  max-height: 2000px;
90
  opacity: 1;
91
  }
92
-
93
  .skill-btn {
94
  background: rgba(255, 80, 197, 0.1);
95
  border: 1px solid rgba(255, 80, 197, 0.3);
96
  transition: all 0.2s ease;
97
  }
98
-
99
  .skill-btn:hover {
100
  background: rgba(255, 80, 197, 0.2);
101
  border-color: rgba(255, 80, 197, 0.5);
102
  transform: translateY(-2px);
103
  }
104
-
105
  .skill-btn.active {
106
  background: var(--neon-cyan);
107
  color: var(--bg-primary);
108
  border-color: var(--neon-cyan);
109
  }
110
-
111
  /* ===== CHAT WIDGET STYLES ===== */
112
  .chat-widget {
113
  position: fixed;
@@ -116,7 +118,7 @@
116
  z-index: 9999;
117
  font-family: 'Inter', sans-serif;
118
  }
119
-
120
  .chat-toggle {
121
  width: 60px;
122
  height: 60px;
@@ -131,18 +133,18 @@
131
  transition: all 0.3s ease;
132
  position: relative;
133
  }
134
-
135
  .chat-toggle:hover {
136
  transform: scale(1.1);
137
  box-shadow: 0 12px 32px rgba(255, 80, 197, 0.6);
138
  }
139
-
140
  .chat-toggle svg {
141
  width: 28px;
142
  height: 28px;
143
  color: white;
144
  }
145
-
146
  .chat-badge {
147
  position: absolute;
148
  top: -4px;
@@ -155,7 +157,7 @@
155
  border-radius: 12px;
156
  box-shadow: 0 2px 8px rgba(0, 255, 136, 0.4);
157
  }
158
-
159
  .chat-window {
160
  position: absolute;
161
  bottom: 80px;
@@ -172,18 +174,19 @@
172
  overflow: hidden;
173
  animation: slideUp 0.3s ease-out;
174
  }
175
-
176
  @keyframes slideUp {
177
  from {
178
  opacity: 0;
179
  transform: translateY(20px);
180
  }
 
181
  to {
182
  opacity: 1;
183
  transform: translateY(0);
184
  }
185
  }
186
-
187
  .chat-header {
188
  background: linear-gradient(135deg, rgba(255, 80, 197, 0.2), rgba(200, 80, 255, 0.2));
189
  border-bottom: 1px solid var(--glass-border);
@@ -192,7 +195,7 @@
192
  align-items: center;
193
  justify-content: space-between;
194
  }
195
-
196
  .chat-avatar {
197
  width: 40px;
198
  height: 40px;
@@ -203,20 +206,20 @@
203
  justify-content: center;
204
  flex-shrink: 0;
205
  }
206
-
207
  .chat-avatar svg {
208
  width: 24px;
209
  height: 24px;
210
  color: white;
211
  }
212
-
213
  .chat-title {
214
  font-size: 16px;
215
  font-weight: 600;
216
  color: var(--text-primary);
217
  margin: 0;
218
  }
219
-
220
  .chat-status {
221
  font-size: 12px;
222
  color: var(--text-muted);
@@ -225,7 +228,7 @@
225
  gap: 6px;
226
  margin: 2px 0 0 0;
227
  }
228
-
229
  .status-dot {
230
  width: 8px;
231
  height: 8px;
@@ -233,12 +236,19 @@
233
  background: var(--status-success);
234
  animation: pulse 2s infinite;
235
  }
236
-
237
  @keyframes pulse {
238
- 0%, 100% { opacity: 1; }
239
- 50% { opacity: 0.5; }
 
 
 
 
 
 
 
240
  }
241
-
242
  .chat-minimize {
243
  background: none;
244
  border: none;
@@ -248,17 +258,17 @@
248
  border-radius: 8px;
249
  transition: all 0.2s;
250
  }
251
-
252
  .chat-minimize:hover {
253
  background: rgba(255, 255, 255, 0.1);
254
  color: var(--text-primary);
255
  }
256
-
257
  .chat-minimize svg {
258
  width: 20px;
259
  height: 20px;
260
  }
261
-
262
  .chat-modes {
263
  display: flex;
264
  gap: 8px;
@@ -266,7 +276,7 @@
266
  border-bottom: 1px solid var(--glass-border);
267
  background: rgba(255, 255, 255, 0.02);
268
  }
269
-
270
  .mode-btn {
271
  flex: 1;
272
  display: flex;
@@ -283,18 +293,18 @@
283
  cursor: pointer;
284
  transition: all 0.2s;
285
  }
286
-
287
  .mode-btn:hover {
288
  background: rgba(255, 255, 255, 0.08);
289
  border-color: rgba(255, 80, 197, 0.3);
290
  }
291
-
292
  .mode-btn.active {
293
  background: linear-gradient(135deg, rgba(255, 80, 197, 0.2), rgba(200, 80, 255, 0.2));
294
  border-color: var(--neon-cyan);
295
  color: var(--neon-cyan);
296
  }
297
-
298
  .chat-messages {
299
  flex: 1;
300
  overflow-y: auto;
@@ -303,37 +313,38 @@
303
  flex-direction: column;
304
  gap: 16px;
305
  }
306
-
307
  .chat-messages::-webkit-scrollbar {
308
  width: 6px;
309
  }
310
-
311
  .chat-messages::-webkit-scrollbar-track {
312
  background: rgba(255, 255, 255, 0.05);
313
  }
314
-
315
  .chat-messages::-webkit-scrollbar-thumb {
316
  background: rgba(255, 80, 197, 0.3);
317
  border-radius: 3px;
318
  }
319
-
320
  .message {
321
  display: flex;
322
  gap: 12px;
323
  animation: fadeIn 0.3s ease-out;
324
  }
325
-
326
  @keyframes fadeIn {
327
  from {
328
  opacity: 0;
329
  transform: translateY(10px);
330
  }
 
331
  to {
332
  opacity: 1;
333
  transform: translateY(0);
334
  }
335
  }
336
-
337
  .message-avatar {
338
  width: 32px;
339
  height: 32px;
@@ -344,17 +355,17 @@
344
  justify-content: center;
345
  flex-shrink: 0;
346
  }
347
-
348
  .message-avatar svg {
349
  width: 18px;
350
  height: 18px;
351
  color: white;
352
  }
353
-
354
  .user-message .message-avatar {
355
  background: linear-gradient(135deg, #667eea, #764ba2);
356
  }
357
-
358
  .message-content {
359
  flex: 1;
360
  background: rgba(255, 255, 255, 0.05);
@@ -365,22 +376,22 @@
365
  font-size: 14px;
366
  line-height: 1.6;
367
  }
368
-
369
  .user-message .message-content {
370
  background: linear-gradient(135deg, rgba(255, 80, 197, 0.15), rgba(200, 80, 255, 0.15));
371
  border-color: rgba(255, 80, 197, 0.3);
372
  }
373
-
374
  .message-list {
375
  margin: 8px 0;
376
  padding-left: 20px;
377
  }
378
-
379
  .message-list li {
380
  margin: 4px 0;
381
  color: var(--text-secondary);
382
  }
383
-
384
  .quick-actions {
385
  display: flex;
386
  gap: 8px;
@@ -389,7 +400,7 @@
389
  background: rgba(255, 255, 255, 0.02);
390
  flex-wrap: wrap;
391
  }
392
-
393
  .quick-action-btn {
394
  display: flex;
395
  align-items: center;
@@ -404,26 +415,26 @@
404
  cursor: pointer;
405
  transition: all 0.2s;
406
  }
407
-
408
  .quick-action-btn:hover {
409
  background: rgba(255, 80, 197, 0.2);
410
  border-color: var(--neon-cyan);
411
  color: var(--neon-cyan);
412
  transform: translateY(-2px);
413
  }
414
-
415
  .chat-input-container {
416
  border-top: 1px solid var(--glass-border);
417
  background: rgba(255, 255, 255, 0.02);
418
  padding: 16px;
419
  }
420
-
421
  .chat-input-wrapper {
422
  display: flex;
423
  gap: 12px;
424
  align-items: flex-end;
425
  }
426
-
427
  .chat-input {
428
  flex: 1;
429
  background: rgba(255, 255, 255, 0.1);
@@ -437,32 +448,32 @@
437
  max-height: 120px;
438
  transition: all 0.2s;
439
  }
440
-
441
  .chat-input:focus {
442
  outline: none;
443
  border-color: var(--neon-cyan);
444
  background: rgba(255, 255, 255, 0.15);
445
  }
446
-
447
  .chat-input::placeholder {
448
  color: rgba(255, 255, 255, 0.5);
449
  }
450
-
451
  /* Light mode input fix */
452
  :root:not(.dark) .chat-input {
453
  background: rgba(0, 0, 0, 0.05);
454
  border-color: rgba(0, 0, 0, 0.1);
455
  color: #0f172a !important;
456
  }
457
-
458
  :root:not(.dark) .chat-input:focus {
459
  background: rgba(0, 0, 0, 0.08);
460
  }
461
-
462
  :root:not(.dark) .chat-input::placeholder {
463
  color: rgba(0, 0, 0, 0.4);
464
  }
465
-
466
  .chat-send {
467
  width: 44px;
468
  height: 44px;
@@ -476,27 +487,27 @@
476
  transition: all 0.2s;
477
  flex-shrink: 0;
478
  }
479
-
480
  .chat-send:hover {
481
  transform: scale(1.05);
482
  box-shadow: 0 4px 12px rgba(255, 80, 197, 0.4);
483
  }
484
-
485
  .chat-send:active {
486
  transform: scale(0.95);
487
  }
488
-
489
  .chat-send svg {
490
  width: 20px;
491
  height: 20px;
492
  color: white;
493
  }
494
-
495
  .loading-spinner-chat {
496
  width: 20px;
497
  height: 20px;
498
  }
499
-
500
  .spinner-ring {
501
  width: 20px;
502
  height: 20px;
@@ -505,18 +516,20 @@
505
  border-radius: 50%;
506
  animation: spin 0.8s linear infinite;
507
  }
508
-
509
  @keyframes spin {
510
- to { transform: rotate(360deg); }
 
 
511
  }
512
-
513
  .chat-footer-text {
514
  text-align: center;
515
  font-size: 11px;
516
  color: var(--text-muted);
517
  margin-top: 8px;
518
  }
519
-
520
  /* Mobile Responsive */
521
  @media (max-width: 768px) {
522
  .chat-window {
@@ -525,13 +538,13 @@
525
  bottom: 80px;
526
  right: 16px;
527
  }
528
-
529
  .chat-widget {
530
  bottom: 16px;
531
  right: 16px;
532
  }
533
  }
534
-
535
  /* ===== PROGRESS CARD STYLES ===== */
536
  .progress-card {
537
  background: var(--glass-bg);
@@ -542,25 +555,26 @@
542
  margin-top: 24px;
543
  animation: slideIn 0.5s ease-out;
544
  }
545
-
546
  @keyframes slideIn {
547
  from {
548
  opacity: 0;
549
  transform: translateY(-20px);
550
  }
 
551
  to {
552
  opacity: 1;
553
  transform: translateY(0);
554
  }
555
  }
556
-
557
  .progress-card-header {
558
  display: flex;
559
  align-items: center;
560
  gap: 16px;
561
  margin-bottom: 24px;
562
  }
563
-
564
  .progress-icon {
565
  width: 56px;
566
  height: 56px;
@@ -571,37 +585,37 @@
571
  justify-content: center;
572
  flex-shrink: 0;
573
  }
574
-
575
  .progress-icon svg {
576
  width: 32px;
577
  height: 32px;
578
  color: white;
579
  }
580
-
581
  .animate-spin {
582
  animation: spin 2s linear infinite;
583
  }
584
-
585
  .progress-title {
586
  font-size: 20px;
587
  font-weight: 700;
588
  color: var(--text-primary);
589
  margin: 0;
590
  }
591
-
592
  .progress-subtitle {
593
  font-size: 14px;
594
  color: var(--text-secondary);
595
  margin: 4px 0 0 0;
596
  }
597
-
598
  .progress-bar-container {
599
  display: flex;
600
  align-items: center;
601
  gap: 16px;
602
  margin-bottom: 24px;
603
  }
604
-
605
  .progress-bar-bg {
606
  flex: 1;
607
  height: 12px;
@@ -610,7 +624,7 @@
610
  overflow: hidden;
611
  position: relative;
612
  }
613
-
614
  .progress-bar-fill {
615
  height: 100%;
616
  background: linear-gradient(90deg, var(--neon-cyan), var(--neon-purple));
@@ -620,7 +634,7 @@
620
  position: relative;
621
  overflow: hidden;
622
  }
623
-
624
  .progress-bar-fill::after {
625
  content: '';
626
  position: absolute;
@@ -628,20 +642,23 @@
628
  left: 0;
629
  right: 0;
630
  bottom: 0;
631
- background: linear-gradient(
632
- 90deg,
633
- transparent,
634
- rgba(255, 255, 255, 0.3),
635
- transparent
636
- );
637
  animation: shimmer 2s infinite;
638
  }
639
-
640
  @keyframes shimmer {
641
- 0% { transform: translateX(-100%); }
642
- 100% { transform: translateX(100%); }
 
 
 
 
 
643
  }
644
-
645
  .progress-percentage {
646
  font-size: 18px;
647
  font-weight: 700;
@@ -649,13 +666,13 @@
649
  min-width: 50px;
650
  text-align: right;
651
  }
652
-
653
  .progress-steps {
654
  display: grid;
655
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
656
  gap: 16px;
657
  }
658
-
659
  .progress-step {
660
  display: flex;
661
  flex-direction: column;
@@ -668,67 +685,67 @@
668
  transition: all 0.3s ease;
669
  opacity: 0.4;
670
  }
671
-
672
  .progress-step.active {
673
  opacity: 1;
674
  background: linear-gradient(135deg, rgba(255, 80, 197, 0.15), rgba(200, 80, 255, 0.15));
675
  border-color: var(--neon-cyan);
676
  transform: scale(1.05);
677
  }
678
-
679
  .progress-step.completed {
680
  opacity: 0.7;
681
  background: rgba(0, 255, 136, 0.1);
682
  border-color: var(--status-success);
683
  }
684
-
685
  .step-icon {
686
  font-size: 32px;
687
  filter: grayscale(100%);
688
  transition: filter 0.3s ease;
689
  }
690
-
691
  .progress-step.active .step-icon,
692
  .progress-step.completed .step-icon {
693
  filter: grayscale(0%);
694
  }
695
-
696
  .step-text {
697
  font-size: 13px;
698
  font-weight: 500;
699
  color: var(--text-secondary);
700
  text-align: center;
701
  }
702
-
703
  .progress-step.active .step-text {
704
  color: var(--neon-cyan);
705
  font-weight: 600;
706
  }
707
-
708
  .progress-step.completed .step-text {
709
  color: var(--status-success);
710
  }
711
-
712
  @media (max-width: 768px) {
713
  .progress-card {
714
  padding: 24px 20px;
715
  }
716
-
717
  .progress-steps {
718
  grid-template-columns: repeat(2, 1fr);
719
  }
720
-
721
  .progress-title {
722
  font-size: 18px;
723
  }
724
  }
725
-
726
  .wave-divider {
727
  position: relative;
728
  height: 70px;
729
  width: 100%;
730
  }
731
-
732
  .wave-divider svg {
733
  position: absolute;
734
  bottom: 0;
@@ -741,12 +758,12 @@
741
  transition: all 0.3s ease;
742
  border-top: 4px solid transparent;
743
  }
744
-
745
  .agent-card:hover {
746
  transform: translateY(-5px);
747
- box-shadow: 0 10px 20px rgba(0,0,0,0.1);
748
  }
749
-
750
  .agent-icon {
751
  width: 50px;
752
  height: 50px;
@@ -757,7 +774,7 @@
757
  margin-right: 15px;
758
  font-size: 24px;
759
  }
760
-
761
  .chat-message {
762
  max-width: 80%;
763
  margin-bottom: 15px;
@@ -766,27 +783,27 @@
766
  position: relative;
767
  animation: fadeIn 0.3s ease-out;
768
  }
769
-
770
  .chat-message.user {
771
  background-color: #ff50c5;
772
  color: white;
773
  margin-left: auto;
774
  border-bottom-right-radius: 4px;
775
  }
776
-
777
  .chat-message.assistant {
778
  background-color: #f3f4f6;
779
  color: #1f2937;
780
  margin-right: auto;
781
  border-bottom-left-radius: 4px;
782
  }
783
-
784
  .typing-indicator {
785
  display: flex;
786
  justify-content: center;
787
  padding: 10px 0;
788
  }
789
-
790
  .typing-indicator span {
791
  display: inline-block;
792
  width: 8px;
@@ -795,44 +812,51 @@
795
  border-radius: 50%;
796
  margin: 0 2px;
797
  }
798
-
799
  @keyframes fadeIn {
800
- from { opacity: 0; transform: translateY(10px); }
801
- to { opacity: 1; transform: translateY(0); }
 
 
 
 
 
 
 
802
  }
803
-
804
  /* Chat interface styles */
805
  .chat-container {
806
  border-radius: 1rem;
807
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
808
  overflow: hidden;
809
  }
810
-
811
  .chat-messages {
812
  max-height: 400px;
813
  overflow-y: auto;
814
  scrollbar-width: thin;
815
  scrollbar-color: #cbd5e0 #edf2f7;
816
  }
817
-
818
  .chat-messages::-webkit-scrollbar {
819
  width: 6px;
820
  }
821
-
822
  .chat-messages::-webkit-scrollbar-track {
823
  background: #edf2f7;
824
  }
825
-
826
  .chat-messages::-webkit-scrollbar-thumb {
827
  background-color: #cbd5e0;
828
  border-radius: 3px;
829
  }
830
-
831
  .chat-input-container {
832
  border-top: 1px solid #e2e8f0;
833
  background: #f8fafc;
834
  }
835
-
836
  .chat-input {
837
  resize: none;
838
  max-height: 120px;
@@ -840,62 +864,63 @@
840
  border: 1px solid #e2e8f0;
841
  transition: border-color 0.2s, box-shadow 0.2s;
842
  }
843
-
844
  .chat-input:focus {
845
  outline: none;
846
  border-color: #a78bfa;
847
  box-shadow: 0 0 0 1px #a78bfa;
848
  }
849
-
850
  .send-button {
851
  transition: all 0.2s;
852
  }
853
-
854
  .send-button:disabled {
855
  opacity: 0.5;
856
  cursor: not-allowed;
857
  }
858
-
859
  .suggestion-button {
860
  transition: all 0.2s;
861
  }
862
-
863
  .suggestion-button:hover {
864
  transform: translateY(-1px);
865
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
866
  }
867
-
868
  /* Responsive adjustments */
869
  @media (max-width: 768px) {
870
  .agent-card {
871
  margin-bottom: 20px;
872
  }
873
-
874
  .chat-message {
875
  max-width: 90%;
876
  }
877
-
878
  .chat-messages {
879
  max-height: 300px;
880
  }
881
  }
882
-
883
  /* Animation for message appearance */
884
  @keyframes messageAppear {
885
  from {
886
  opacity: 0;
887
  transform: translateY(10px);
888
  }
 
889
  to {
890
  opacity: 1;
891
  transform: translateY(0);
892
  }
893
  }
894
-
895
  .message {
896
  animation: messageAppear 0.3s ease-out forwards;
897
  }
898
-
899
  .loading-spinner {
900
  display: none;
901
  border: 3px solid #f3f3f3;
@@ -906,37 +931,50 @@
906
  animation: spin 1s linear infinite;
907
  margin: 0 auto;
908
  }
909
-
910
  @keyframes spin {
911
- 0% { transform: rotate(0deg); }
912
- 100% { transform: rotate(360deg); }
 
 
 
 
 
913
  }
914
-
915
  @keyframes float {
916
- 0% { transform: translateY(0px); }
917
- 50% { transform: translateY(-20px); }
918
- 100% { transform: translateY(0px); }
 
 
 
 
 
 
 
 
919
  }
920
-
921
  .float-animation {
922
  animation: float 6s ease-in-out infinite;
923
  }
924
-
925
  .card-hover {
926
  transition: all 0.3s ease;
927
  }
928
-
929
  .card-hover:hover {
930
  transform: translateY(-10px);
931
  box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
932
  }
933
-
934
  .btn-hover {
935
  position: relative;
936
  overflow: hidden;
937
  transition: all 0.3s ease;
938
  }
939
-
940
  .btn-hover:after {
941
  content: '';
942
  position: absolute;
@@ -944,10 +982,10 @@
944
  left: -100%;
945
  width: 100%;
946
  height: 100%;
947
- background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
948
  transition: all 0.5s ease;
949
  }
950
-
951
  .btn-hover:hover:after {
952
  left: 100%;
953
  }
@@ -957,6 +995,7 @@
957
  .nav-stripe a {
958
  position: relative;
959
  }
 
960
  .nav-stripe a::after {
961
  content: "";
962
  position: absolute;
@@ -964,16 +1003,19 @@
964
  bottom: -4px;
965
  width: 100%;
966
  height: 3px;
967
- background-color: #ff50c5; /* magenta */
 
968
  transform: scaleX(0);
969
  transform-origin: left;
970
  transition: transform 0.3s ease;
971
  }
 
972
  .nav-stripe a:hover::after {
973
  transform: scaleX(1);
974
  }
975
  </style>
976
  </head>
 
977
  <body class="grid-background min-h-screen">
978
  <!-- Navigation -->
979
  <nav class="glass-nav py-4 px-6">
@@ -983,251 +1025,286 @@
983
  </a>
984
  <div class="hidden md:flex items-center gap-6">
985
  {% if current_user.is_authenticated %}
986
- <a href="/dashboard" class="text-secondary hover:text-neon-cyan transition">Dashboard</a>
987
- <a href="{{ url_for('auth.logout') }}" class="text-secondary hover:text-neon-cyan transition">Logout</a>
988
  {% else %}
989
- <a href="{{ url_for('auth.login') }}" class="text-secondary hover:text-neon-cyan transition">Login</a>
990
- <a href="{{ url_for('auth.register') }}" class="neon-btn-sm">Register</a>
991
  {% endif %}
992
  </div>
993
  <!-- Desktop dark mode toggle -->
994
- <button id="theme-toggle" class="ml-2 inline-flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-magenta" aria-label="Toggle dark mode">
995
- <svg id="theme-toggle-light-icon" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
996
- <path d="M10 15a5 5 0 100-10 5 5 0 000 10zM10 1a1 1 0 011 1v1a1 1 0 11-2 0V2a1 1 0 011-1zm0 14a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zm9-5a1 1 0 01-1 1h-1a1 1 0 110-2h1a1 1 0 011 1zM3 10a1 1 0 01-1 1H1a1 1 0 110-2h1a1 1 0 011 1zm12.364-6.364a1 1 0 010 1.414L14.95 6.464a1 1 0 01-1.414-1.414l1.414-1.414a1 1 0 011.414 0zM5.05 14.95a1 1 0 011.414 0l1.414-1.414a1 1 0 10-1.414-1.414L5.05 13.536a1 1 0 010 1.414zm9.9 0a1 1 0 10-1.414-1.414l-1.414 1.414a1 1 0 101.414 1.414l1.414-1.414zM5.05 5.05a1 1 0 011.414 0L7.878 6.464A1 1 0 116.464 7.878L5.05 6.464A1 1 0 015.05 5.05z" clip-rule="evenodd"></path>
 
 
 
 
 
997
  </svg>
998
- <svg id="theme-toggle-dark-icon" class="w-5 h-5 hidden" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
 
999
  <path d="M17.293 13.293A8 8 0 016.707 2.707a8 8 0 1010.586 10.586z"></path>
1000
  </svg>
1001
  </button>
1002
  </div>
1003
- <!-- Mobile menu button -->
1004
- <button id="mobile-menu-button" class="md:hidden text-secondary" aria-expanded="false">
1005
- <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1006
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
1007
- </svg>
1008
- </button>
 
1009
  </div>
1010
-
1011
  <!-- Mobile Menu -->
1012
  <div id="mobile-menu" class="hidden md:hidden mt-4 space-y-2">
1013
  {% if current_user.is_authenticated %}
1014
- <a href="/dashboard" class="block text-secondary hover:text-neon-cyan transition">Dashboard</a>
1015
- <a href="{{ url_for('auth.logout') }}" class="block text-secondary hover:text-neon-cyan transition">Logout</a>
 
1016
  {% else %}
1017
- <a href="{{ url_for('auth.login') }}" class="block text-secondary hover:text-neon-cyan transition">Login</a>
1018
- <a href="{{ url_for('auth.register') }}" class="block neon-btn-sm">Register</a>
1019
  {% endif %}
1020
  <!-- Mobile dark mode toggle -->
1021
- <button id="theme-toggle-mobile" class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-magenta" aria-label="Toggle dark mode">
1022
- <svg id="theme-toggle-mobile-light-icon" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
1023
- <path d="M10 15a5 5 0 100-10 5 5 0 000 10zM10 1a1 1 0 011 1v1a1 1 0 11-2 0V2a1 1 0 011-1zm0 14a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zm9-5a1 1 0 01-1 1h-1a1 1 0 110-2h1a1 1 0 011 1zM3 10a1 1 0 01-1 1H1a1 1 0 110-2h1a1 1 0 011 1zm12.364-6.364a1 1 0 010 1.414L14.95 6.464a1 1 0 01-1.414-1.414l1.414-1.414a1 1 0 011.414 0zM5.05 14.95a1 1 0 011.414 0l1.414-1.414a1 1 0 10-1.414-1.414L5.05 13.536a1 1 0 010 1.414zm9.9 0a1 1 0 10-1.414-1.414l-1.414 1.414a1 1 0 101.414 1.414l1.414-1.414zM5.05 5.05a1 1 0 011.414 0L7.878 6.464A1 1 0 116.464 7.878L5.05 6.464A1 1 0 015.05 5.05z" clip-rule="evenodd"></path>
 
 
 
 
 
1024
  </svg>
1025
- <svg id="theme-toggle-mobile-dark-icon" class="w-5 h-5 hidden" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
 
1026
  <path d="M17.293 13.293A8 8 0 016.707 2.707a8 8 0 1010.586 10.586z"></path>
1027
  </svg>
1028
  </button>
1029
  </div>
1030
  </nav>
1031
-
1032
- <!-- Hero Section -->
1033
- <section class="py-20 px-6">
1034
- <div class="container mx-auto text-center max-w-4xl">
1035
- <h1 class="text-6xl font-bold text-white mb-6">
1036
- AI Learning Path<br><span class="text-neon-cyan">Generator</span>
1037
- </h1>
1038
- <p class="text-2xl text-secondary mb-12">
1039
- Create personalized learning journeys powered by AI
1040
- </p>
1041
- <a href="#path-form" class="neon-btn text-lg">Start Your Journey</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
1042
  </div>
1043
- </section>
1044
-
1045
- <!-- Form Section -->
1046
- <section id="path-form" class="py-16 px-6">
1047
- <div class="container mx-auto max-w-4xl">
1048
- <h2 class="text-4xl font-bold mb-12 text-center text-white">Create Your <span class="text-neon-cyan">Learning Path</span></h2>
1049
-
1050
- <!-- Error message (if any) -->
1051
- {% if error %}
1052
- <div class="bg-red-50 text-red-800 p-4 rounded-lg mb-6">
1053
- {{ error }}
1054
- </div>
1055
- {% endif %}
1056
-
1057
- <div class="glass-card p-8">
1058
- <form id="pathGeneratorForm" class="space-y-6" action="/generate" method="POST">
1059
- <!-- Expertise Level -->
1060
- <div>
1061
- <label for="expertise_level" class="block text-lg font-medium text-secondary mb-2">Your current expertise level</label>
1062
- <div class="select-wrapper">
1063
- <select id="expertise_level" name="expertise_level" class="glass-select" required>
1064
- {% for level, description in expertise_levels.items() %}
1065
- <option value="{{ level }}">{{ level.title() }} - {{ description }}</option>
1066
- {% endfor %}
1067
- </select>
1068
- </div>
1069
  </div>
1070
-
1071
- <!-- Topic with Categories -->
1072
- <div>
1073
- <label for="topic" class="block text-lg font-medium text-secondary mb-2">What do you want to learn?</label>
1074
-
1075
- <!-- Custom Topic Input (moved to top) -->
1076
- <input type="text" id="topic" name="topic" class="glass-input mb-4" placeholder="Type a topic or select from categories below..." required>
1077
-
1078
- <!-- Collapsible Categories with All Skills -->
1079
- <div class="mb-4">
1080
- <h4 class="text-sm font-medium text-secondary mb-3">Browse by Category:</h4>
1081
- <div class="space-y-2">
1082
- {% for category in categories %}
1083
- <div class="category-accordion glass-card-no-hover">
1084
- <button type="button" class="category-header w-full text-left px-4 py-3 flex items-center justify-between hover:bg-white/5 transition-all rounded-lg" data-category="{{ category }}">
1085
- <span class="flex items-center gap-2 text-secondary font-medium">
1086
- <span class="text-xl">
1087
- {% if 'Cloud' in category %}☁️
1088
- {% elif 'Data Science' in category or 'AI' in category %}🤖
1089
- {% elif 'Web' in category %}🌐
1090
- {% elif 'Mobile' in category %}📱
1091
- {% elif 'Design' in category %}🎨
1092
- {% elif 'Business' in category %}💼
1093
- {% else %}💡
1094
- {% endif %}
1095
- </span>
1096
- <span>{{ category }}</span>
1097
- <span class="text-xs text-muted">({{ skills_by_category[category]|length }} skills)</span>
 
 
1098
  </span>
1099
- <svg class="category-arrow w-5 h-5 text-secondary transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1100
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
1101
- </svg>
1102
- </button>
1103
- <div class="category-content hidden px-4 pb-4">
1104
- <div class="flex flex-wrap gap-2 mt-2">
1105
- {% for skill in skills_by_category[category] %}
1106
- <button type="button" class="skill-btn topic-btn text-sm" data-topic="{{ skill }}">
1107
- {{ skill }}
1108
- </button>
1109
- {% endfor %}
1110
- </div>
 
 
 
 
 
 
1111
  </div>
1112
  </div>
1113
- {% endfor %}
1114
  </div>
 
1115
  </div>
1116
-
1117
- <!-- Quick Access: Most Popular Skills -->
1118
- <div class="mb-4">
1119
- <button type="button" id="showPopularSkills" class="text-sm text-neon-cyan hover:text-neon-purple transition-colors flex items-center gap-1">
1120
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1121
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
1122
- </svg>
1123
- Quick Access: Popular Skills
 
 
 
 
 
 
 
 
1124
  </button>
1125
- <div id="popularSkillsContainer" class="hidden mt-3 flex flex-wrap gap-2">
1126
- {% for skill in all_skills[:15] %}
1127
- <button type="button" class="skill-btn topic-btn text-sm" data-topic="{{ skill }}">
1128
- {{ skill }}
1129
- </button>
1130
- {% endfor %}
1131
- </div>
1132
  </div>
1133
  </div>
1134
-
1135
- <!-- Learning Style -->
1136
- <div>
1137
- <label for="learning_style" class="block text-lg font-medium text-secondary mb-2">Your Learning Style</label>
1138
- <div class="select-wrapper mb-4">
1139
- <select id="learning_style" name="learning_style" class="glass-select" required>
1140
- <option value="visual">Visual - Learn best through images, diagrams, and spatial understanding</option>
1141
- <option value="auditory">Auditory - Learn best through listening and speaking</option>
1142
- <option value="reading">Reading/Writing - Learn best through written materials and note-taking</option>
1143
- <option value="kinesthetic">Kinesthetic - Learn best through hands-on activities and physical interaction</option>
1144
- </select>
1145
- </div>
 
 
 
 
1146
  </div>
1147
-
1148
- <!-- Duration in Weeks -->
1149
- <div>
1150
- <label for="duration_weeks" class="block text-lg font-medium text-secondary mb-2">
1151
- Duration (in weeks)
1152
- </label>
1153
- <input type="number" id="duration_weeks" name="duration_weeks" min="1" max="52" required
1154
- class="glass-input"
1155
- placeholder="e.g., 4">
1156
- <p class="mt-1 text-sm text-muted">How many weeks do you plan to study this topic?</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1157
  </div>
1158
-
1159
- <!-- Time Commitment -->
1160
  <div>
1161
- <label for="time_commitment" class="block text-lg font-medium text-secondary mb-2">How much time can you commit weekly?</label>
1162
- <div class="select-wrapper">
1163
- <select id="time_commitment" name="time_commitment" class="glass-select" required>
1164
- {% for commitment, description in time_commitments.items() %}
1165
- <option value="{{ commitment }}">{{ commitment.title() }} - {{ description }}</option>
1166
- {% endfor %}
1167
- </select>
1168
- </div>
1169
  </div>
1170
-
1171
-
1172
- <!-- Submit Button -->
1173
- <div class="pt-6">
1174
- <button type="submit" id="generateBtn" class="neon-btn w-full py-4 text-lg font-bold flex items-center justify-center gap-3">
1175
- <span id="btnText">Generate My Learning Path</span>
1176
- </button>
1177
  </div>
1178
- </form>
1179
-
1180
- <!-- Progress Card (Hidden by default) -->
1181
- <div id="progressCard" class="progress-card hidden">
1182
- <div class="progress-card-header">
1183
- <div class="progress-icon">
1184
- <svg class="animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1185
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
1186
- </svg>
1187
- </div>
1188
- <div>
1189
- <h3 class="progress-title">Generating Your Learning Path</h3>
1190
- <p class="progress-subtitle" id="progressStatus">Initializing AI...</p>
1191
- </div>
1192
  </div>
1193
-
1194
- <div class="progress-bar-container">
1195
- <div class="progress-bar-bg">
1196
- <div class="progress-bar-fill" id="progressBar"></div>
1197
- </div>
1198
- <div class="progress-percentage" id="progressPercentage">0%</div>
1199
  </div>
1200
-
1201
- <div class="progress-steps">
1202
- <div class="progress-step" id="step1">
1203
- <div class="step-icon">🔍</div>
1204
- <div class="step-text">Analyzing Requirements</div>
1205
- </div>
1206
- <div class="progress-step" id="step2">
1207
- <div class="step-icon">🤖</div>
1208
- <div class="step-text">AI Processing</div>
1209
- </div>
1210
- <div class="progress-step" id="step3">
1211
- <div class="step-icon">📚</div>
1212
- <div class="step-text">Curating Resources</div>
1213
- </div>
1214
- <div class="progress-step" id="step4">
1215
- <div class="step-icon">✨</div>
1216
- <div class="step-text">Finalizing Path</div>
1217
- </div>
1218
  </div>
1219
  </div>
1220
  </div>
1221
  </div>
1222
- </section>
1223
-
1224
-
 
1225
  <!-- AI Chat Widget -->
1226
  <div id="chatWidget" class="chat-widget">
1227
  <!-- Chat Button -->
1228
  <button id="chatToggle" class="chat-toggle" aria-label="Open AI Assistant">
1229
  <svg class="chat-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1230
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path>
 
 
1231
  </svg>
1232
  <svg class="close-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1233
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
@@ -1242,7 +1319,9 @@
1242
  <div class="flex items-center gap-3">
1243
  <div class="chat-avatar">
1244
  <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
1245
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
 
 
1246
  </svg>
1247
  </div>
1248
  <div>
@@ -1264,19 +1343,24 @@
1264
  <div class="chat-modes">
1265
  <button class="mode-btn active" data-mode="general">
1266
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1267
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
 
 
1268
  </svg>
1269
  Chat
1270
  </button>
1271
  <button class="mode-btn" data-mode="path">
1272
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1273
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"></path>
 
 
1274
  </svg>
1275
  Path
1276
  </button>
1277
  <button class="mode-btn" data-mode="research">
1278
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1279
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
 
1280
  </svg>
1281
  Research
1282
  </button>
@@ -1287,7 +1371,9 @@
1287
  <div class="message bot-message">
1288
  <div class="message-avatar">
1289
  <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
1290
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
 
 
1291
  </svg>
1292
  </div>
1293
  <div class="message-content">
@@ -1313,13 +1399,16 @@
1313
  </button>
1314
  <button class="quick-action-btn" data-action="explore-skills">
1315
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1316
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
 
1317
  </svg>
1318
  Explore Skills
1319
  </button>
1320
  <button class="quick-action-btn" data-action="salary-info">
1321
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1322
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
 
 
1323
  </svg>
1324
  Salary Info
1325
  </button>
@@ -1328,15 +1417,11 @@
1328
  <!-- Chat Input -->
1329
  <div class="chat-input-container">
1330
  <div class="chat-input-wrapper">
1331
- <textarea
1332
- id="chatInput"
1333
- class="chat-input"
1334
- placeholder="Ask me anything..."
1335
- rows="1"
1336
- ></textarea>
1337
  <button id="chatSend" class="chat-send" aria-label="Send message">
1338
  <svg class="send-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1339
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
 
1340
  </svg>
1341
  <div class="loading-spinner-chat hidden">
1342
  <div class="spinner-ring"></div>
@@ -1355,7 +1440,7 @@
1355
  <p>Cpyright 2026</p>
1356
  </div>
1357
  </footer>
1358
-
1359
  <!-- External Scripts -->
1360
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
1361
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
@@ -1369,10 +1454,10 @@
1369
  mangle: false
1370
  });
1371
  </script>
1372
-
1373
  <!-- Main Application Script -->
1374
  <script>
1375
- document.addEventListener('DOMContentLoaded', function() {
1376
  // Form submission with animated progress card
1377
  const pathGeneratorForm = document.getElementById('pathGeneratorForm');
1378
  const generateBtn = document.getElementById('generateBtn');
@@ -1382,23 +1467,23 @@
1382
  const progressStatus = document.getElementById('progressStatus');
1383
 
1384
  if (pathGeneratorForm) {
1385
- pathGeneratorForm.addEventListener('submit', function(e) {
1386
  // Show progress card
1387
  if (progressCard) {
1388
  progressCard.classList.remove('hidden');
1389
-
1390
  // Scroll to progress card
1391
  setTimeout(() => {
1392
  progressCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
1393
  }, 100);
1394
  }
1395
-
1396
  // Disable button
1397
  if (generateBtn) {
1398
  generateBtn.disabled = true;
1399
  generateBtn.style.opacity = '0.5';
1400
  }
1401
-
1402
  // Animate progress through stages
1403
  animateProgress();
1404
  });
@@ -1417,7 +1502,7 @@
1417
  function updateStage() {
1418
  if (currentStage < stages.length) {
1419
  const stage = stages[currentStage];
1420
-
1421
  // Update progress bar
1422
  if (progressBar) {
1423
  progressBar.style.width = stage.progress + '%';
@@ -1428,7 +1513,7 @@
1428
  if (progressStatus) {
1429
  progressStatus.textContent = stage.status;
1430
  }
1431
-
1432
  // Mark previous steps as completed
1433
  for (let i = 1; i < stage.step; i++) {
1434
  const stepEl = document.getElementById('step' + i);
@@ -1437,13 +1522,13 @@
1437
  stepEl.classList.add('completed');
1438
  }
1439
  }
1440
-
1441
  // Mark current step as active
1442
  const currentStepEl = document.getElementById('step' + stage.step);
1443
  if (currentStepEl) {
1444
  currentStepEl.classList.add('active');
1445
  }
1446
-
1447
  currentStage++;
1448
  setTimeout(updateStage, stage.duration);
1449
  }
@@ -1457,7 +1542,7 @@
1457
  const mobileMenu = document.getElementById('mobile-menu');
1458
 
1459
  if (mobileMenuButton) {
1460
- mobileMenuButton.addEventListener('click', function() {
1461
  if (mobileMenu) {
1462
  const isExpanded = mobileMenuButton.getAttribute('aria-expanded') === 'true';
1463
  mobileMenuButton.setAttribute('aria-expanded', !isExpanded);
@@ -1468,13 +1553,13 @@
1468
 
1469
  // Accordion functionality for categories
1470
  const categoryHeaders = document.querySelectorAll('.category-header');
1471
-
1472
  categoryHeaders.forEach(header => {
1473
- header.addEventListener('click', function() {
1474
  const accordion = this.closest('.category-accordion');
1475
  const content = accordion.querySelector('.category-content');
1476
  const isActive = accordion.classList.contains('active');
1477
-
1478
  // Close all other accordions
1479
  document.querySelectorAll('.category-accordion').forEach(acc => {
1480
  if (acc !== accordion) {
@@ -1484,7 +1569,7 @@
1484
  otherContent.classList.add('hidden');
1485
  }
1486
  });
1487
-
1488
  // Toggle current accordion
1489
  if (isActive) {
1490
  accordion.classList.remove('active');
@@ -1497,13 +1582,13 @@
1497
  }
1498
  });
1499
  });
1500
-
1501
  // Popular Skills toggle
1502
  const showPopularBtn = document.getElementById('showPopularSkills');
1503
  const popularContainer = document.getElementById('popularSkillsContainer');
1504
-
1505
  if (showPopularBtn && popularContainer) {
1506
- showPopularBtn.addEventListener('click', function() {
1507
  popularContainer.classList.toggle('hidden');
1508
  const icon = this.querySelector('svg');
1509
  if (popularContainer.classList.contains('hidden')) {
@@ -1517,23 +1602,23 @@
1517
  }
1518
  });
1519
  }
1520
-
1521
  // Skill button handlers
1522
  const topicInput = document.getElementById('topic');
1523
-
1524
  // Use event delegation for skill buttons (more efficient)
1525
- document.addEventListener('click', function(e) {
1526
  if (e.target.classList.contains('skill-btn')) {
1527
  const topic = e.target.getAttribute('data-topic') || e.target.textContent.trim();
1528
-
1529
  // Remove active from all skill buttons
1530
  document.querySelectorAll('.skill-btn').forEach(btn => {
1531
  btn.classList.remove('active');
1532
  });
1533
-
1534
  // Set active on clicked button
1535
  e.target.classList.add('active');
1536
-
1537
  // Set the topic input value
1538
  if (topicInput) {
1539
  topicInput.value = topic;
@@ -1551,21 +1636,21 @@
1551
  const chatMessages = document.getElementById('chatMessages');
1552
  const modeButtons = document.querySelectorAll('.mode-btn');
1553
  const quickActionButtons = document.querySelectorAll('.quick-action-btn');
1554
-
1555
  let currentMode = 'general';
1556
  let conversationHistory = [];
1557
 
1558
  // Toggle chat window
1559
- chatToggle.addEventListener('click', function() {
1560
  const isHidden = chatWindow.classList.contains('hidden');
1561
  chatWindow.classList.toggle('hidden');
1562
-
1563
  // Toggle icons
1564
  const chatIcon = chatToggle.querySelector('.chat-icon');
1565
  const closeIcon = chatToggle.querySelector('.close-icon');
1566
  chatIcon.classList.toggle('hidden');
1567
  closeIcon.classList.toggle('hidden');
1568
-
1569
  if (!isHidden) {
1570
  // Closing
1571
  chatToggle.style.transform = 'rotate(0deg)';
@@ -1580,7 +1665,7 @@
1580
  });
1581
 
1582
  // Minimize chat
1583
- chatMinimize.addEventListener('click', function() {
1584
  chatWindow.classList.add('hidden');
1585
  const chatIcon = chatToggle.querySelector('.chat-icon');
1586
  const closeIcon = chatToggle.querySelector('.close-icon');
@@ -1590,11 +1675,11 @@
1590
 
1591
  // Mode switching
1592
  modeButtons.forEach(btn => {
1593
- btn.addEventListener('click', function() {
1594
  modeButtons.forEach(b => b.classList.remove('active'));
1595
  this.classList.add('active');
1596
  currentMode = this.getAttribute('data-mode');
1597
-
1598
  // Update placeholder based on mode
1599
  if (currentMode === 'general') {
1600
  chatInput.placeholder = 'Ask me anything...';
@@ -1608,9 +1693,9 @@
1608
 
1609
  // Quick actions
1610
  quickActionButtons.forEach(btn => {
1611
- btn.addEventListener('click', function() {
1612
  const action = this.getAttribute('data-action');
1613
-
1614
  if (action === 'create-path') {
1615
  chatInput.value = 'I want to create a learning path for ';
1616
  chatInput.focus();
@@ -1628,13 +1713,13 @@
1628
  });
1629
 
1630
  // Auto-resize textarea
1631
- chatInput.addEventListener('input', function() {
1632
  this.style.height = 'auto';
1633
  this.style.height = Math.min(this.scrollHeight, 120) + 'px';
1634
  });
1635
 
1636
  // Send message on Enter (Shift+Enter for new line)
1637
- chatInput.addEventListener('keydown', function(e) {
1638
  if (e.key === 'Enter' && !e.shiftKey) {
1639
  e.preventDefault();
1640
  sendMessage();
@@ -1651,7 +1736,7 @@
1651
 
1652
  // Add user message to UI
1653
  addMessageToUI(message, 'user');
1654
-
1655
  // Clear input
1656
  if (!customMessage) {
1657
  chatInput.value = '';
@@ -1683,7 +1768,7 @@
1683
  if (data.success) {
1684
  const botMessage = data.response || data.data?.answer || 'I received your message!';
1685
  addMessageToUI(botMessage, 'bot');
1686
-
1687
  // Update conversation history
1688
  conversationHistory.push({
1689
  role: 'user',
@@ -1713,16 +1798,16 @@
1713
  function addMessageToUI(message, sender) {
1714
  const messageDiv = document.createElement('div');
1715
  messageDiv.className = `message ${sender}-message`;
1716
-
1717
- const avatarSvg = sender === 'bot'
1718
  ? '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path></svg>'
1719
  : '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>';
1720
-
1721
  messageDiv.innerHTML = `
1722
  <div class="message-avatar">${avatarSvg}</div>
1723
  <div class="message-content">${formatMessage(message)}</div>
1724
  `;
1725
-
1726
  chatMessages.appendChild(messageDiv);
1727
  chatMessages.scrollTop = chatMessages.scrollHeight;
1728
  }
@@ -1740,7 +1825,7 @@
1740
  function showLoading(show) {
1741
  const sendIcon = chatSend.querySelector('.send-icon');
1742
  const loadingSpinner = chatSend.querySelector('.loading-spinner-chat');
1743
-
1744
  if (show) {
1745
  sendIcon.classList.add('hidden');
1746
  loadingSpinner.classList.remove('hidden');
@@ -1753,17 +1838,18 @@
1753
  }
1754
  });
1755
  </script>
1756
- <script src="{{ url_for('static', filename='js/theme.js') }}"></script>
1757
- {% if scroll_to_form %}
1758
- <script>
1759
- // Auto-scroll to the form when coming from /new-path route
1760
- document.addEventListener('DOMContentLoaded', function() {
1761
- const pathForm = document.getElementById('path-form');
1762
- if (pathForm) {
1763
- pathForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
1764
- }
1765
- });
1766
- </script>
1767
- {% endif %}
1768
  </body>
1769
- </html>
 
 
1
  <!DOCTYPE html>
2
  <html lang="en" class="dark">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <title>AI Learning Path Generator</title>
8
  <!-- Initialize theme immediately to prevent flash -->
9
  <script>
10
+ (function () {
11
  const theme = localStorage.getItem('theme');
12
  if (theme === 'light') {
13
  document.documentElement.classList.remove('dark');
 
33
  font-weight: 600;
34
  box-shadow: 0 0 0 2px var(--tw-ring-color, currentColor);
35
  }
36
+
37
+ .bg-magenta,
38
+ .magenta-bg {
39
  background-color: #ff50c5 !important;
40
  }
41
+
42
  .text-magenta {
43
  color: #ff50c5 !important;
44
  }
45
+
46
  .yellow-bg {
47
  background-color: #F9C846;
48
  }
49
+
50
  /* Ensure footer uses proper background */
51
  footer {
52
  background-color: rgba(31, 41, 55, 0.95) !important;
53
  }
54
+
55
  /* Category Accordion Styles */
56
  .category-accordion {
57
  border: 1px solid var(--glass-border);
 
59
  overflow: hidden;
60
  transition: all 0.3s ease;
61
  }
62
+
63
  .category-accordion:hover {
64
  border-color: rgba(255, 80, 197, 0.4);
65
  }
66
+
67
  .category-header {
68
  cursor: pointer;
69
  user-select: none;
70
  }
71
+
72
  .category-header:active {
73
  transform: scale(0.98);
74
  }
75
+
76
  .category-accordion.active .category-arrow {
77
  transform: rotate(180deg);
78
  }
79
+
80
  .category-content {
81
  transition: max-height 0.4s ease-out, opacity 0.3s ease-out;
82
  overflow: hidden;
83
  }
84
+
85
  .category-content.hidden {
86
  max-height: 0 !important;
87
  opacity: 0;
88
  }
89
+
90
  .category-content.show {
91
  max-height: 2000px;
92
  opacity: 1;
93
  }
94
+
95
  .skill-btn {
96
  background: rgba(255, 80, 197, 0.1);
97
  border: 1px solid rgba(255, 80, 197, 0.3);
98
  transition: all 0.2s ease;
99
  }
100
+
101
  .skill-btn:hover {
102
  background: rgba(255, 80, 197, 0.2);
103
  border-color: rgba(255, 80, 197, 0.5);
104
  transform: translateY(-2px);
105
  }
106
+
107
  .skill-btn.active {
108
  background: var(--neon-cyan);
109
  color: var(--bg-primary);
110
  border-color: var(--neon-cyan);
111
  }
112
+
113
  /* ===== CHAT WIDGET STYLES ===== */
114
  .chat-widget {
115
  position: fixed;
 
118
  z-index: 9999;
119
  font-family: 'Inter', sans-serif;
120
  }
121
+
122
  .chat-toggle {
123
  width: 60px;
124
  height: 60px;
 
133
  transition: all 0.3s ease;
134
  position: relative;
135
  }
136
+
137
  .chat-toggle:hover {
138
  transform: scale(1.1);
139
  box-shadow: 0 12px 32px rgba(255, 80, 197, 0.6);
140
  }
141
+
142
  .chat-toggle svg {
143
  width: 28px;
144
  height: 28px;
145
  color: white;
146
  }
147
+
148
  .chat-badge {
149
  position: absolute;
150
  top: -4px;
 
157
  border-radius: 12px;
158
  box-shadow: 0 2px 8px rgba(0, 255, 136, 0.4);
159
  }
160
+
161
  .chat-window {
162
  position: absolute;
163
  bottom: 80px;
 
174
  overflow: hidden;
175
  animation: slideUp 0.3s ease-out;
176
  }
177
+
178
  @keyframes slideUp {
179
  from {
180
  opacity: 0;
181
  transform: translateY(20px);
182
  }
183
+
184
  to {
185
  opacity: 1;
186
  transform: translateY(0);
187
  }
188
  }
189
+
190
  .chat-header {
191
  background: linear-gradient(135deg, rgba(255, 80, 197, 0.2), rgba(200, 80, 255, 0.2));
192
  border-bottom: 1px solid var(--glass-border);
 
195
  align-items: center;
196
  justify-content: space-between;
197
  }
198
+
199
  .chat-avatar {
200
  width: 40px;
201
  height: 40px;
 
206
  justify-content: center;
207
  flex-shrink: 0;
208
  }
209
+
210
  .chat-avatar svg {
211
  width: 24px;
212
  height: 24px;
213
  color: white;
214
  }
215
+
216
  .chat-title {
217
  font-size: 16px;
218
  font-weight: 600;
219
  color: var(--text-primary);
220
  margin: 0;
221
  }
222
+
223
  .chat-status {
224
  font-size: 12px;
225
  color: var(--text-muted);
 
228
  gap: 6px;
229
  margin: 2px 0 0 0;
230
  }
231
+
232
  .status-dot {
233
  width: 8px;
234
  height: 8px;
 
236
  background: var(--status-success);
237
  animation: pulse 2s infinite;
238
  }
239
+
240
  @keyframes pulse {
241
+
242
+ 0%,
243
+ 100% {
244
+ opacity: 1;
245
+ }
246
+
247
+ 50% {
248
+ opacity: 0.5;
249
+ }
250
  }
251
+
252
  .chat-minimize {
253
  background: none;
254
  border: none;
 
258
  border-radius: 8px;
259
  transition: all 0.2s;
260
  }
261
+
262
  .chat-minimize:hover {
263
  background: rgba(255, 255, 255, 0.1);
264
  color: var(--text-primary);
265
  }
266
+
267
  .chat-minimize svg {
268
  width: 20px;
269
  height: 20px;
270
  }
271
+
272
  .chat-modes {
273
  display: flex;
274
  gap: 8px;
 
276
  border-bottom: 1px solid var(--glass-border);
277
  background: rgba(255, 255, 255, 0.02);
278
  }
279
+
280
  .mode-btn {
281
  flex: 1;
282
  display: flex;
 
293
  cursor: pointer;
294
  transition: all 0.2s;
295
  }
296
+
297
  .mode-btn:hover {
298
  background: rgba(255, 255, 255, 0.08);
299
  border-color: rgba(255, 80, 197, 0.3);
300
  }
301
+
302
  .mode-btn.active {
303
  background: linear-gradient(135deg, rgba(255, 80, 197, 0.2), rgba(200, 80, 255, 0.2));
304
  border-color: var(--neon-cyan);
305
  color: var(--neon-cyan);
306
  }
307
+
308
  .chat-messages {
309
  flex: 1;
310
  overflow-y: auto;
 
313
  flex-direction: column;
314
  gap: 16px;
315
  }
316
+
317
  .chat-messages::-webkit-scrollbar {
318
  width: 6px;
319
  }
320
+
321
  .chat-messages::-webkit-scrollbar-track {
322
  background: rgba(255, 255, 255, 0.05);
323
  }
324
+
325
  .chat-messages::-webkit-scrollbar-thumb {
326
  background: rgba(255, 80, 197, 0.3);
327
  border-radius: 3px;
328
  }
329
+
330
  .message {
331
  display: flex;
332
  gap: 12px;
333
  animation: fadeIn 0.3s ease-out;
334
  }
335
+
336
  @keyframes fadeIn {
337
  from {
338
  opacity: 0;
339
  transform: translateY(10px);
340
  }
341
+
342
  to {
343
  opacity: 1;
344
  transform: translateY(0);
345
  }
346
  }
347
+
348
  .message-avatar {
349
  width: 32px;
350
  height: 32px;
 
355
  justify-content: center;
356
  flex-shrink: 0;
357
  }
358
+
359
  .message-avatar svg {
360
  width: 18px;
361
  height: 18px;
362
  color: white;
363
  }
364
+
365
  .user-message .message-avatar {
366
  background: linear-gradient(135deg, #667eea, #764ba2);
367
  }
368
+
369
  .message-content {
370
  flex: 1;
371
  background: rgba(255, 255, 255, 0.05);
 
376
  font-size: 14px;
377
  line-height: 1.6;
378
  }
379
+
380
  .user-message .message-content {
381
  background: linear-gradient(135deg, rgba(255, 80, 197, 0.15), rgba(200, 80, 255, 0.15));
382
  border-color: rgba(255, 80, 197, 0.3);
383
  }
384
+
385
  .message-list {
386
  margin: 8px 0;
387
  padding-left: 20px;
388
  }
389
+
390
  .message-list li {
391
  margin: 4px 0;
392
  color: var(--text-secondary);
393
  }
394
+
395
  .quick-actions {
396
  display: flex;
397
  gap: 8px;
 
400
  background: rgba(255, 255, 255, 0.02);
401
  flex-wrap: wrap;
402
  }
403
+
404
  .quick-action-btn {
405
  display: flex;
406
  align-items: center;
 
415
  cursor: pointer;
416
  transition: all 0.2s;
417
  }
418
+
419
  .quick-action-btn:hover {
420
  background: rgba(255, 80, 197, 0.2);
421
  border-color: var(--neon-cyan);
422
  color: var(--neon-cyan);
423
  transform: translateY(-2px);
424
  }
425
+
426
  .chat-input-container {
427
  border-top: 1px solid var(--glass-border);
428
  background: rgba(255, 255, 255, 0.02);
429
  padding: 16px;
430
  }
431
+
432
  .chat-input-wrapper {
433
  display: flex;
434
  gap: 12px;
435
  align-items: flex-end;
436
  }
437
+
438
  .chat-input {
439
  flex: 1;
440
  background: rgba(255, 255, 255, 0.1);
 
448
  max-height: 120px;
449
  transition: all 0.2s;
450
  }
451
+
452
  .chat-input:focus {
453
  outline: none;
454
  border-color: var(--neon-cyan);
455
  background: rgba(255, 255, 255, 0.15);
456
  }
457
+
458
  .chat-input::placeholder {
459
  color: rgba(255, 255, 255, 0.5);
460
  }
461
+
462
  /* Light mode input fix */
463
  :root:not(.dark) .chat-input {
464
  background: rgba(0, 0, 0, 0.05);
465
  border-color: rgba(0, 0, 0, 0.1);
466
  color: #0f172a !important;
467
  }
468
+
469
  :root:not(.dark) .chat-input:focus {
470
  background: rgba(0, 0, 0, 0.08);
471
  }
472
+
473
  :root:not(.dark) .chat-input::placeholder {
474
  color: rgba(0, 0, 0, 0.4);
475
  }
476
+
477
  .chat-send {
478
  width: 44px;
479
  height: 44px;
 
487
  transition: all 0.2s;
488
  flex-shrink: 0;
489
  }
490
+
491
  .chat-send:hover {
492
  transform: scale(1.05);
493
  box-shadow: 0 4px 12px rgba(255, 80, 197, 0.4);
494
  }
495
+
496
  .chat-send:active {
497
  transform: scale(0.95);
498
  }
499
+
500
  .chat-send svg {
501
  width: 20px;
502
  height: 20px;
503
  color: white;
504
  }
505
+
506
  .loading-spinner-chat {
507
  width: 20px;
508
  height: 20px;
509
  }
510
+
511
  .spinner-ring {
512
  width: 20px;
513
  height: 20px;
 
516
  border-radius: 50%;
517
  animation: spin 0.8s linear infinite;
518
  }
519
+
520
  @keyframes spin {
521
+ to {
522
+ transform: rotate(360deg);
523
+ }
524
  }
525
+
526
  .chat-footer-text {
527
  text-align: center;
528
  font-size: 11px;
529
  color: var(--text-muted);
530
  margin-top: 8px;
531
  }
532
+
533
  /* Mobile Responsive */
534
  @media (max-width: 768px) {
535
  .chat-window {
 
538
  bottom: 80px;
539
  right: 16px;
540
  }
541
+
542
  .chat-widget {
543
  bottom: 16px;
544
  right: 16px;
545
  }
546
  }
547
+
548
  /* ===== PROGRESS CARD STYLES ===== */
549
  .progress-card {
550
  background: var(--glass-bg);
 
555
  margin-top: 24px;
556
  animation: slideIn 0.5s ease-out;
557
  }
558
+
559
  @keyframes slideIn {
560
  from {
561
  opacity: 0;
562
  transform: translateY(-20px);
563
  }
564
+
565
  to {
566
  opacity: 1;
567
  transform: translateY(0);
568
  }
569
  }
570
+
571
  .progress-card-header {
572
  display: flex;
573
  align-items: center;
574
  gap: 16px;
575
  margin-bottom: 24px;
576
  }
577
+
578
  .progress-icon {
579
  width: 56px;
580
  height: 56px;
 
585
  justify-content: center;
586
  flex-shrink: 0;
587
  }
588
+
589
  .progress-icon svg {
590
  width: 32px;
591
  height: 32px;
592
  color: white;
593
  }
594
+
595
  .animate-spin {
596
  animation: spin 2s linear infinite;
597
  }
598
+
599
  .progress-title {
600
  font-size: 20px;
601
  font-weight: 700;
602
  color: var(--text-primary);
603
  margin: 0;
604
  }
605
+
606
  .progress-subtitle {
607
  font-size: 14px;
608
  color: var(--text-secondary);
609
  margin: 4px 0 0 0;
610
  }
611
+
612
  .progress-bar-container {
613
  display: flex;
614
  align-items: center;
615
  gap: 16px;
616
  margin-bottom: 24px;
617
  }
618
+
619
  .progress-bar-bg {
620
  flex: 1;
621
  height: 12px;
 
624
  overflow: hidden;
625
  position: relative;
626
  }
627
+
628
  .progress-bar-fill {
629
  height: 100%;
630
  background: linear-gradient(90deg, var(--neon-cyan), var(--neon-purple));
 
634
  position: relative;
635
  overflow: hidden;
636
  }
637
+
638
  .progress-bar-fill::after {
639
  content: '';
640
  position: absolute;
 
642
  left: 0;
643
  right: 0;
644
  bottom: 0;
645
+ background: linear-gradient(90deg,
646
+ transparent,
647
+ rgba(255, 255, 255, 0.3),
648
+ transparent);
 
 
649
  animation: shimmer 2s infinite;
650
  }
651
+
652
  @keyframes shimmer {
653
+ 0% {
654
+ transform: translateX(-100%);
655
+ }
656
+
657
+ 100% {
658
+ transform: translateX(100%);
659
+ }
660
  }
661
+
662
  .progress-percentage {
663
  font-size: 18px;
664
  font-weight: 700;
 
666
  min-width: 50px;
667
  text-align: right;
668
  }
669
+
670
  .progress-steps {
671
  display: grid;
672
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
673
  gap: 16px;
674
  }
675
+
676
  .progress-step {
677
  display: flex;
678
  flex-direction: column;
 
685
  transition: all 0.3s ease;
686
  opacity: 0.4;
687
  }
688
+
689
  .progress-step.active {
690
  opacity: 1;
691
  background: linear-gradient(135deg, rgba(255, 80, 197, 0.15), rgba(200, 80, 255, 0.15));
692
  border-color: var(--neon-cyan);
693
  transform: scale(1.05);
694
  }
695
+
696
  .progress-step.completed {
697
  opacity: 0.7;
698
  background: rgba(0, 255, 136, 0.1);
699
  border-color: var(--status-success);
700
  }
701
+
702
  .step-icon {
703
  font-size: 32px;
704
  filter: grayscale(100%);
705
  transition: filter 0.3s ease;
706
  }
707
+
708
  .progress-step.active .step-icon,
709
  .progress-step.completed .step-icon {
710
  filter: grayscale(0%);
711
  }
712
+
713
  .step-text {
714
  font-size: 13px;
715
  font-weight: 500;
716
  color: var(--text-secondary);
717
  text-align: center;
718
  }
719
+
720
  .progress-step.active .step-text {
721
  color: var(--neon-cyan);
722
  font-weight: 600;
723
  }
724
+
725
  .progress-step.completed .step-text {
726
  color: var(--status-success);
727
  }
728
+
729
  @media (max-width: 768px) {
730
  .progress-card {
731
  padding: 24px 20px;
732
  }
733
+
734
  .progress-steps {
735
  grid-template-columns: repeat(2, 1fr);
736
  }
737
+
738
  .progress-title {
739
  font-size: 18px;
740
  }
741
  }
742
+
743
  .wave-divider {
744
  position: relative;
745
  height: 70px;
746
  width: 100%;
747
  }
748
+
749
  .wave-divider svg {
750
  position: absolute;
751
  bottom: 0;
 
758
  transition: all 0.3s ease;
759
  border-top: 4px solid transparent;
760
  }
761
+
762
  .agent-card:hover {
763
  transform: translateY(-5px);
764
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
765
  }
766
+
767
  .agent-icon {
768
  width: 50px;
769
  height: 50px;
 
774
  margin-right: 15px;
775
  font-size: 24px;
776
  }
777
+
778
  .chat-message {
779
  max-width: 80%;
780
  margin-bottom: 15px;
 
783
  position: relative;
784
  animation: fadeIn 0.3s ease-out;
785
  }
786
+
787
  .chat-message.user {
788
  background-color: #ff50c5;
789
  color: white;
790
  margin-left: auto;
791
  border-bottom-right-radius: 4px;
792
  }
793
+
794
  .chat-message.assistant {
795
  background-color: #f3f4f6;
796
  color: #1f2937;
797
  margin-right: auto;
798
  border-bottom-left-radius: 4px;
799
  }
800
+
801
  .typing-indicator {
802
  display: flex;
803
  justify-content: center;
804
  padding: 10px 0;
805
  }
806
+
807
  .typing-indicator span {
808
  display: inline-block;
809
  width: 8px;
 
812
  border-radius: 50%;
813
  margin: 0 2px;
814
  }
815
+
816
  @keyframes fadeIn {
817
+ from {
818
+ opacity: 0;
819
+ transform: translateY(10px);
820
+ }
821
+
822
+ to {
823
+ opacity: 1;
824
+ transform: translateY(0);
825
+ }
826
  }
827
+
828
  /* Chat interface styles */
829
  .chat-container {
830
  border-radius: 1rem;
831
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
832
  overflow: hidden;
833
  }
834
+
835
  .chat-messages {
836
  max-height: 400px;
837
  overflow-y: auto;
838
  scrollbar-width: thin;
839
  scrollbar-color: #cbd5e0 #edf2f7;
840
  }
841
+
842
  .chat-messages::-webkit-scrollbar {
843
  width: 6px;
844
  }
845
+
846
  .chat-messages::-webkit-scrollbar-track {
847
  background: #edf2f7;
848
  }
849
+
850
  .chat-messages::-webkit-scrollbar-thumb {
851
  background-color: #cbd5e0;
852
  border-radius: 3px;
853
  }
854
+
855
  .chat-input-container {
856
  border-top: 1px solid #e2e8f0;
857
  background: #f8fafc;
858
  }
859
+
860
  .chat-input {
861
  resize: none;
862
  max-height: 120px;
 
864
  border: 1px solid #e2e8f0;
865
  transition: border-color 0.2s, box-shadow 0.2s;
866
  }
867
+
868
  .chat-input:focus {
869
  outline: none;
870
  border-color: #a78bfa;
871
  box-shadow: 0 0 0 1px #a78bfa;
872
  }
873
+
874
  .send-button {
875
  transition: all 0.2s;
876
  }
877
+
878
  .send-button:disabled {
879
  opacity: 0.5;
880
  cursor: not-allowed;
881
  }
882
+
883
  .suggestion-button {
884
  transition: all 0.2s;
885
  }
886
+
887
  .suggestion-button:hover {
888
  transform: translateY(-1px);
889
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
890
  }
891
+
892
  /* Responsive adjustments */
893
  @media (max-width: 768px) {
894
  .agent-card {
895
  margin-bottom: 20px;
896
  }
897
+
898
  .chat-message {
899
  max-width: 90%;
900
  }
901
+
902
  .chat-messages {
903
  max-height: 300px;
904
  }
905
  }
906
+
907
  /* Animation for message appearance */
908
  @keyframes messageAppear {
909
  from {
910
  opacity: 0;
911
  transform: translateY(10px);
912
  }
913
+
914
  to {
915
  opacity: 1;
916
  transform: translateY(0);
917
  }
918
  }
919
+
920
  .message {
921
  animation: messageAppear 0.3s ease-out forwards;
922
  }
923
+
924
  .loading-spinner {
925
  display: none;
926
  border: 3px solid #f3f3f3;
 
931
  animation: spin 1s linear infinite;
932
  margin: 0 auto;
933
  }
934
+
935
  @keyframes spin {
936
+ 0% {
937
+ transform: rotate(0deg);
938
+ }
939
+
940
+ 100% {
941
+ transform: rotate(360deg);
942
+ }
943
  }
944
+
945
  @keyframes float {
946
+ 0% {
947
+ transform: translateY(0px);
948
+ }
949
+
950
+ 50% {
951
+ transform: translateY(-20px);
952
+ }
953
+
954
+ 100% {
955
+ transform: translateY(0px);
956
+ }
957
  }
958
+
959
  .float-animation {
960
  animation: float 6s ease-in-out infinite;
961
  }
962
+
963
  .card-hover {
964
  transition: all 0.3s ease;
965
  }
966
+
967
  .card-hover:hover {
968
  transform: translateY(-10px);
969
  box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
970
  }
971
+
972
  .btn-hover {
973
  position: relative;
974
  overflow: hidden;
975
  transition: all 0.3s ease;
976
  }
977
+
978
  .btn-hover:after {
979
  content: '';
980
  position: absolute;
 
982
  left: -100%;
983
  width: 100%;
984
  height: 100%;
985
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
986
  transition: all 0.5s ease;
987
  }
988
+
989
  .btn-hover:hover:after {
990
  left: 100%;
991
  }
 
995
  .nav-stripe a {
996
  position: relative;
997
  }
998
+
999
  .nav-stripe a::after {
1000
  content: "";
1001
  position: absolute;
 
1003
  bottom: -4px;
1004
  width: 100%;
1005
  height: 3px;
1006
+ background-color: #ff50c5;
1007
+ /* magenta */
1008
  transform: scaleX(0);
1009
  transform-origin: left;
1010
  transition: transform 0.3s ease;
1011
  }
1012
+
1013
  .nav-stripe a:hover::after {
1014
  transform: scaleX(1);
1015
  }
1016
  </style>
1017
  </head>
1018
+
1019
  <body class="grid-background min-h-screen">
1020
  <!-- Navigation -->
1021
  <nav class="glass-nav py-4 px-6">
 
1025
  </a>
1026
  <div class="hidden md:flex items-center gap-6">
1027
  {% if current_user.is_authenticated %}
1028
+ <a href="/dashboard" class="text-secondary hover:text-neon-cyan transition">Dashboard</a>
1029
+ <a href="{{ url_for('auth.logout') }}" class="text-secondary hover:text-neon-cyan transition">Logout</a>
1030
  {% else %}
1031
+ <a href="{{ url_for('auth.login') }}" class="text-secondary hover:text-neon-cyan transition">Login</a>
1032
+ <a href="{{ url_for('auth.register') }}" class="neon-btn-sm">Register</a>
1033
  {% endif %}
1034
  </div>
1035
  <!-- Desktop dark mode toggle -->
1036
+ <button id="theme-toggle"
1037
+ class="ml-2 inline-flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-magenta"
1038
+ aria-label="Toggle dark mode">
1039
+ <svg id="theme-toggle-light-icon" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"
1040
+ xmlns="http://www.w3.org/2000/svg">
1041
+ <path
1042
+ d="M10 15a5 5 0 100-10 5 5 0 000 10zM10 1a1 1 0 011 1v1a1 1 0 11-2 0V2a1 1 0 011-1zm0 14a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zm9-5a1 1 0 01-1 1h-1a1 1 0 110-2h1a1 1 0 011 1zM3 10a1 1 0 01-1 1H1a1 1 0 110-2h1a1 1 0 011 1zm12.364-6.364a1 1 0 010 1.414L14.95 6.464a1 1 0 01-1.414-1.414l1.414-1.414a1 1 0 011.414 0zM5.05 14.95a1 1 0 011.414 0l1.414-1.414a1 1 0 10-1.414-1.414L5.05 13.536a1 1 0 010 1.414zm9.9 0a1 1 0 10-1.414-1.414l-1.414 1.414a1 1 0 101.414 1.414l1.414-1.414zM5.05 5.05a1 1 0 011.414 0L7.878 6.464A1 1 0 116.464 7.878L5.05 6.464A1 1 0 015.05 5.05z"
1043
+ clip-rule="evenodd"></path>
1044
  </svg>
1045
+ <svg id="theme-toggle-dark-icon" class="w-5 h-5 hidden" fill="currentColor" viewBox="0 0 20 20"
1046
+ xmlns="http://www.w3.org/2000/svg">
1047
  <path d="M17.293 13.293A8 8 0 016.707 2.707a8 8 0 1010.586 10.586z"></path>
1048
  </svg>
1049
  </button>
1050
  </div>
1051
+ <!-- Mobile menu button -->
1052
+ <button id="mobile-menu-button" class="md:hidden text-secondary" aria-expanded="false">
1053
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1054
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16">
1055
+ </path>
1056
+ </svg>
1057
+ </button>
1058
  </div>
1059
+
1060
  <!-- Mobile Menu -->
1061
  <div id="mobile-menu" class="hidden md:hidden mt-4 space-y-2">
1062
  {% if current_user.is_authenticated %}
1063
+ <a href="/dashboard" class="block text-secondary hover:text-neon-cyan transition">Dashboard</a>
1064
+ <a href="{{ url_for('auth.logout') }}"
1065
+ class="block text-secondary hover:text-neon-cyan transition">Logout</a>
1066
  {% else %}
1067
+ <a href="{{ url_for('auth.login') }}" class="block text-secondary hover:text-neon-cyan transition">Login</a>
1068
+ <a href="{{ url_for('auth.register') }}" class="block neon-btn-sm">Register</a>
1069
  {% endif %}
1070
  <!-- Mobile dark mode toggle -->
1071
+ <button id="theme-toggle-mobile"
1072
+ class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-magenta"
1073
+ aria-label="Toggle dark mode">
1074
+ <svg id="theme-toggle-mobile-light-icon" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"
1075
+ xmlns="http://www.w3.org/2000/svg">
1076
+ <path
1077
+ d="M10 15a5 5 0 100-10 5 5 0 000 10zM10 1a1 1 0 011 1v1a1 1 0 11-2 0V2a1 1 0 011-1zm0 14a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zm9-5a1 1 0 01-1 1h-1a1 1 0 110-2h1a1 1 0 011 1zM3 10a1 1 0 01-1 1H1a1 1 0 110-2h1a1 1 0 011 1zm12.364-6.364a1 1 0 010 1.414L14.95 6.464a1 1 0 01-1.414-1.414l1.414-1.414a1 1 0 011.414 0zM5.05 14.95a1 1 0 011.414 0l1.414-1.414a1 1 0 10-1.414-1.414L5.05 13.536a1 1 0 010 1.414zm9.9 0a1 1 0 10-1.414-1.414l-1.414 1.414a1 1 0 101.414 1.414l1.414-1.414zM5.05 5.05a1 1 0 011.414 0L7.878 6.464A1 1 0 116.464 7.878L5.05 6.464A1 1 0 015.05 5.05z"
1078
+ clip-rule="evenodd"></path>
1079
  </svg>
1080
+ <svg id="theme-toggle-mobile-dark-icon" class="w-5 h-5 hidden" fill="currentColor" viewBox="0 0 20 20"
1081
+ xmlns="http://www.w3.org/2000/svg">
1082
  <path d="M17.293 13.293A8 8 0 016.707 2.707a8 8 0 1010.586 10.586z"></path>
1083
  </svg>
1084
  </button>
1085
  </div>
1086
  </nav>
1087
+
1088
+ <!-- Hero Section -->
1089
+ <section class="py-20 px-6">
1090
+ <div class="container mx-auto text-center max-w-4xl">
1091
+ <h1 class="text-6xl font-bold text-white mb-6">
1092
+ AI Learning Path<br><span class="text-neon-cyan">Generator</span>
1093
+ </h1>
1094
+ <p class="text-2xl text-secondary mb-12">
1095
+ Create personalized learning journeys powered by AI
1096
+ </p>
1097
+ <a href="#path-form" class="neon-btn text-lg">Start Your Journey</a>
1098
+ </div>
1099
+ </section>
1100
+
1101
+ <!-- Form Section -->
1102
+ <section id="path-form" class="py-16 px-6">
1103
+ <div class="container mx-auto max-w-4xl">
1104
+ <h2 class="text-4xl font-bold mb-12 text-center text-white">Create Your <span
1105
+ class="text-neon-cyan">Learning Path</span></h2>
1106
+
1107
+ <!-- Error message (if any) -->
1108
+ {% if error %}
1109
+ <div class="bg-red-50 text-red-800 p-4 rounded-lg mb-6">
1110
+ {{ error }}
1111
  </div>
1112
+ {% endif %}
1113
+
1114
+ <div class="glass-card p-8">
1115
+ <form id="pathGeneratorForm" class="space-y-6" action="/generate" method="POST">
1116
+ <!-- Expertise Level -->
1117
+ <div>
1118
+ <label for="expertise_level" class="block text-lg font-medium text-secondary mb-2">Your current
1119
+ expertise level</label>
1120
+ <div class="select-wrapper">
1121
+ <select id="expertise_level" name="expertise_level" class="glass-select" required>
1122
+ {% for level, description in expertise_levels.items() %}
1123
+ <option value="{{ level }}">{{ level.title() }} - {{ description }}</option>
1124
+ {% endfor %}
1125
+ </select>
 
 
 
 
 
 
 
 
 
 
 
 
1126
  </div>
1127
+ </div>
1128
+
1129
+ <!-- Topic with Categories -->
1130
+ <div>
1131
+ <label for="topic" class="block text-lg font-medium text-secondary mb-2">What do you want to
1132
+ learn?</label>
1133
+
1134
+ <!-- Custom Topic Input (moved to top) -->
1135
+ <input type="text" id="topic" name="topic" class="glass-input mb-4"
1136
+ placeholder="Type a topic or select from categories below..." required>
1137
+
1138
+ <!-- Collapsible Categories with All Skills -->
1139
+ <div class="mb-4">
1140
+ <h4 class="text-sm font-medium text-secondary mb-3">Browse by Category:</h4>
1141
+ <div class="space-y-2">
1142
+ {% for category in categories %}
1143
+ <div class="category-accordion glass-card-no-hover">
1144
+ <button type="button"
1145
+ class="category-header w-full text-left px-4 py-3 flex items-center justify-between hover:bg-white/5 transition-all rounded-lg"
1146
+ data-category="{{ category }}">
1147
+ <span class="flex items-center gap-2 text-secondary font-medium">
1148
+ <span class="text-xl">
1149
+ {% if 'Cloud' in category %}☁️
1150
+ {% elif 'Data Science' in category or 'AI' in category %}🤖
1151
+ {% elif 'Web' in category %}🌐
1152
+ {% elif 'Mobile' in category %}📱
1153
+ {% elif 'Design' in category %}🎨
1154
+ {% elif 'Business' in category %}💼
1155
+ {% else %}💡
1156
+ {% endif %}
1157
  </span>
1158
+ <span>{{ category }}</span>
1159
+ <span class="text-xs text-muted">({{ skills_by_category[category]|length }}
1160
+ skills)</span>
1161
+ </span>
1162
+ <svg class="category-arrow w-5 h-5 text-secondary transition-transform duration-300"
1163
+ fill="none" stroke="currentColor" viewBox="0 0 24 24">
1164
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1165
+ d="M19 9l-7 7-7-7"></path>
1166
+ </svg>
1167
+ </button>
1168
+ <div class="category-content hidden px-4 pb-4">
1169
+ <div class="flex flex-wrap gap-2 mt-2">
1170
+ {% for skill in skills_by_category[category] %}
1171
+ <button type="button" class="skill-btn topic-btn text-sm"
1172
+ data-topic="{{ skill }}">
1173
+ {{ skill }}
1174
+ </button>
1175
+ {% endfor %}
1176
  </div>
1177
  </div>
 
1178
  </div>
1179
+ {% endfor %}
1180
  </div>
1181
+ </div>
1182
+
1183
+ <!-- Quick Access: Most Popular Skills -->
1184
+ <div class="mb-4">
1185
+ <button type="button" id="showPopularSkills"
1186
+ class="text-sm text-neon-cyan hover:text-neon-purple transition-colors flex items-center gap-1">
1187
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1188
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1189
+ d="M13 10V3L4 14h7v7l9-11h-7z"></path>
1190
+ </svg>
1191
+ Quick Access: Popular Skills
1192
+ </button>
1193
+ <div id="popularSkillsContainer" class="hidden mt-3 flex flex-wrap gap-2">
1194
+ {% for skill in all_skills[:15] %}
1195
+ <button type="button" class="skill-btn topic-btn text-sm" data-topic="{{ skill }}">
1196
+ {{ skill }}
1197
  </button>
1198
+ {% endfor %}
 
 
 
 
 
 
1199
  </div>
1200
  </div>
1201
+ </div>
1202
+
1203
+ <!-- Learning Style -->
1204
+ <div>
1205
+ <label for="learning_style" class="block text-lg font-medium text-secondary mb-2">Your Learning
1206
+ Style</label>
1207
+ <div class="select-wrapper mb-4">
1208
+ <select id="learning_style" name="learning_style" class="glass-select" required>
1209
+ <option value="visual">Visual - Learn best through images, diagrams, and spatial
1210
+ understanding</option>
1211
+ <option value="auditory">Auditory - Learn best through listening and speaking</option>
1212
+ <option value="reading">Reading/Writing - Learn best through written materials and
1213
+ note-taking</option>
1214
+ <option value="kinesthetic">Kinesthetic - Learn best through hands-on activities and
1215
+ physical interaction</option>
1216
+ </select>
1217
  </div>
1218
+ </div>
1219
+
1220
+ <!-- Duration in Weeks -->
1221
+ <div>
1222
+ <label for="duration_weeks" class="block text-lg font-medium text-secondary mb-2">
1223
+ Duration (in weeks)
1224
+ </label>
1225
+ <input type="number" id="duration_weeks" name="duration_weeks" min="1" max="52" required
1226
+ class="glass-input" placeholder="e.g., 4">
1227
+ <p class="mt-1 text-sm text-muted">How many weeks do you plan to study this topic?</p>
1228
+ </div>
1229
+
1230
+ <!-- Time Commitment -->
1231
+ <div>
1232
+ <label for="time_commitment" class="block text-lg font-medium text-secondary mb-2">How much time
1233
+ can you commit weekly?</label>
1234
+ <div class="select-wrapper">
1235
+ <select id="time_commitment" name="time_commitment" class="glass-select" required>
1236
+ {% for commitment, description in time_commitments.items() %}
1237
+ <option value="{{ commitment }}">{{ commitment.title() }} - {{ description }}</option>
1238
+ {% endfor %}
1239
+ </select>
1240
+ </div>
1241
+ </div>
1242
+
1243
+
1244
+ <!-- Submit Button -->
1245
+ <div class="pt-6">
1246
+ <button type="submit" id="generateBtn"
1247
+ class="neon-btn w-full py-4 text-lg font-bold flex items-center justify-center gap-3">
1248
+ <span id="btnText">Generate My Learning Path</span>
1249
+ </button>
1250
+ </div>
1251
+ </form>
1252
+
1253
+ <!-- Progress Card (Hidden by default) -->
1254
+ <div id="progressCard" class="progress-card hidden">
1255
+ <div class="progress-card-header">
1256
+ <div class="progress-icon">
1257
+ <svg class="animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1258
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1259
+ d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15">
1260
+ </path>
1261
+ </svg>
1262
  </div>
 
 
1263
  <div>
1264
+ <h3 class="progress-title">Generating Your Learning Path</h3>
1265
+ <p class="progress-subtitle" id="progressStatus">Initializing AI...</p>
 
 
 
 
 
 
1266
  </div>
1267
+ </div>
1268
+
1269
+ <div class="progress-bar-container">
1270
+ <div class="progress-bar-bg">
1271
+ <div class="progress-bar-fill" id="progressBar"></div>
 
 
1272
  </div>
1273
+ <div class="progress-percentage" id="progressPercentage">0%</div>
1274
+ </div>
1275
+
1276
+ <div class="progress-steps">
1277
+ <div class="progress-step" id="step1">
1278
+ <div class="step-icon">🔍</div>
1279
+ <div class="step-text">Analyzing Requirements</div>
 
 
 
 
 
 
 
1280
  </div>
1281
+ <div class="progress-step" id="step2">
1282
+ <div class="step-icon">🤖</div>
1283
+ <div class="step-text">AI Processing</div>
 
 
 
1284
  </div>
1285
+ <div class="progress-step" id="step3">
1286
+ <div class="step-icon">📚</div>
1287
+ <div class="step-text">Curating Resources</div>
1288
+ </div>
1289
+ <div class="progress-step" id="step4">
1290
+ <div class="step-icon">✨</div>
1291
+ <div class="step-text">Finalizing Path</div>
 
 
 
 
 
 
 
 
 
 
 
1292
  </div>
1293
  </div>
1294
  </div>
1295
  </div>
1296
+ </div>
1297
+ </section>
1298
+
1299
+
1300
  <!-- AI Chat Widget -->
1301
  <div id="chatWidget" class="chat-widget">
1302
  <!-- Chat Button -->
1303
  <button id="chatToggle" class="chat-toggle" aria-label="Open AI Assistant">
1304
  <svg class="chat-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1305
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1306
+ d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z">
1307
+ </path>
1308
  </svg>
1309
  <svg class="close-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1310
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
 
1319
  <div class="flex items-center gap-3">
1320
  <div class="chat-avatar">
1321
  <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
1322
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1323
+ d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z">
1324
+ </path>
1325
  </svg>
1326
  </div>
1327
  <div>
 
1343
  <div class="chat-modes">
1344
  <button class="mode-btn active" data-mode="general">
1345
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1346
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1347
+ d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z">
1348
+ </path>
1349
  </svg>
1350
  Chat
1351
  </button>
1352
  <button class="mode-btn" data-mode="path">
1353
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1354
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1355
+ d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7">
1356
+ </path>
1357
  </svg>
1358
  Path
1359
  </button>
1360
  <button class="mode-btn" data-mode="research">
1361
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1362
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1363
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
1364
  </svg>
1365
  Research
1366
  </button>
 
1371
  <div class="message bot-message">
1372
  <div class="message-avatar">
1373
  <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
1374
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1375
+ d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z">
1376
+ </path>
1377
  </svg>
1378
  </div>
1379
  <div class="message-content">
 
1399
  </button>
1400
  <button class="quick-action-btn" data-action="explore-skills">
1401
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1402
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1403
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
1404
  </svg>
1405
  Explore Skills
1406
  </button>
1407
  <button class="quick-action-btn" data-action="salary-info">
1408
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1409
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1410
+ d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z">
1411
+ </path>
1412
  </svg>
1413
  Salary Info
1414
  </button>
 
1417
  <!-- Chat Input -->
1418
  <div class="chat-input-container">
1419
  <div class="chat-input-wrapper">
1420
+ <textarea id="chatInput" class="chat-input" placeholder="Ask me anything..." rows="1"></textarea>
 
 
 
 
 
1421
  <button id="chatSend" class="chat-send" aria-label="Send message">
1422
  <svg class="send-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1423
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
1424
+ d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
1425
  </svg>
1426
  <div class="loading-spinner-chat hidden">
1427
  <div class="spinner-ring"></div>
 
1440
  <p>Cpyright 2026</p>
1441
  </div>
1442
  </footer>
1443
+
1444
  <!-- External Scripts -->
1445
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
1446
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
 
1454
  mangle: false
1455
  });
1456
  </script>
1457
+
1458
  <!-- Main Application Script -->
1459
  <script>
1460
+ document.addEventListener('DOMContentLoaded', function () {
1461
  // Form submission with animated progress card
1462
  const pathGeneratorForm = document.getElementById('pathGeneratorForm');
1463
  const generateBtn = document.getElementById('generateBtn');
 
1467
  const progressStatus = document.getElementById('progressStatus');
1468
 
1469
  if (pathGeneratorForm) {
1470
+ pathGeneratorForm.addEventListener('submit', function (e) {
1471
  // Show progress card
1472
  if (progressCard) {
1473
  progressCard.classList.remove('hidden');
1474
+
1475
  // Scroll to progress card
1476
  setTimeout(() => {
1477
  progressCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
1478
  }, 100);
1479
  }
1480
+
1481
  // Disable button
1482
  if (generateBtn) {
1483
  generateBtn.disabled = true;
1484
  generateBtn.style.opacity = '0.5';
1485
  }
1486
+
1487
  // Animate progress through stages
1488
  animateProgress();
1489
  });
 
1502
  function updateStage() {
1503
  if (currentStage < stages.length) {
1504
  const stage = stages[currentStage];
1505
+
1506
  // Update progress bar
1507
  if (progressBar) {
1508
  progressBar.style.width = stage.progress + '%';
 
1513
  if (progressStatus) {
1514
  progressStatus.textContent = stage.status;
1515
  }
1516
+
1517
  // Mark previous steps as completed
1518
  for (let i = 1; i < stage.step; i++) {
1519
  const stepEl = document.getElementById('step' + i);
 
1522
  stepEl.classList.add('completed');
1523
  }
1524
  }
1525
+
1526
  // Mark current step as active
1527
  const currentStepEl = document.getElementById('step' + stage.step);
1528
  if (currentStepEl) {
1529
  currentStepEl.classList.add('active');
1530
  }
1531
+
1532
  currentStage++;
1533
  setTimeout(updateStage, stage.duration);
1534
  }
 
1542
  const mobileMenu = document.getElementById('mobile-menu');
1543
 
1544
  if (mobileMenuButton) {
1545
+ mobileMenuButton.addEventListener('click', function () {
1546
  if (mobileMenu) {
1547
  const isExpanded = mobileMenuButton.getAttribute('aria-expanded') === 'true';
1548
  mobileMenuButton.setAttribute('aria-expanded', !isExpanded);
 
1553
 
1554
  // Accordion functionality for categories
1555
  const categoryHeaders = document.querySelectorAll('.category-header');
1556
+
1557
  categoryHeaders.forEach(header => {
1558
+ header.addEventListener('click', function () {
1559
  const accordion = this.closest('.category-accordion');
1560
  const content = accordion.querySelector('.category-content');
1561
  const isActive = accordion.classList.contains('active');
1562
+
1563
  // Close all other accordions
1564
  document.querySelectorAll('.category-accordion').forEach(acc => {
1565
  if (acc !== accordion) {
 
1569
  otherContent.classList.add('hidden');
1570
  }
1571
  });
1572
+
1573
  // Toggle current accordion
1574
  if (isActive) {
1575
  accordion.classList.remove('active');
 
1582
  }
1583
  });
1584
  });
1585
+
1586
  // Popular Skills toggle
1587
  const showPopularBtn = document.getElementById('showPopularSkills');
1588
  const popularContainer = document.getElementById('popularSkillsContainer');
1589
+
1590
  if (showPopularBtn && popularContainer) {
1591
+ showPopularBtn.addEventListener('click', function () {
1592
  popularContainer.classList.toggle('hidden');
1593
  const icon = this.querySelector('svg');
1594
  if (popularContainer.classList.contains('hidden')) {
 
1602
  }
1603
  });
1604
  }
1605
+
1606
  // Skill button handlers
1607
  const topicInput = document.getElementById('topic');
1608
+
1609
  // Use event delegation for skill buttons (more efficient)
1610
+ document.addEventListener('click', function (e) {
1611
  if (e.target.classList.contains('skill-btn')) {
1612
  const topic = e.target.getAttribute('data-topic') || e.target.textContent.trim();
1613
+
1614
  // Remove active from all skill buttons
1615
  document.querySelectorAll('.skill-btn').forEach(btn => {
1616
  btn.classList.remove('active');
1617
  });
1618
+
1619
  // Set active on clicked button
1620
  e.target.classList.add('active');
1621
+
1622
  // Set the topic input value
1623
  if (topicInput) {
1624
  topicInput.value = topic;
 
1636
  const chatMessages = document.getElementById('chatMessages');
1637
  const modeButtons = document.querySelectorAll('.mode-btn');
1638
  const quickActionButtons = document.querySelectorAll('.quick-action-btn');
1639
+
1640
  let currentMode = 'general';
1641
  let conversationHistory = [];
1642
 
1643
  // Toggle chat window
1644
+ chatToggle.addEventListener('click', function () {
1645
  const isHidden = chatWindow.classList.contains('hidden');
1646
  chatWindow.classList.toggle('hidden');
1647
+
1648
  // Toggle icons
1649
  const chatIcon = chatToggle.querySelector('.chat-icon');
1650
  const closeIcon = chatToggle.querySelector('.close-icon');
1651
  chatIcon.classList.toggle('hidden');
1652
  closeIcon.classList.toggle('hidden');
1653
+
1654
  if (!isHidden) {
1655
  // Closing
1656
  chatToggle.style.transform = 'rotate(0deg)';
 
1665
  });
1666
 
1667
  // Minimize chat
1668
+ chatMinimize.addEventListener('click', function () {
1669
  chatWindow.classList.add('hidden');
1670
  const chatIcon = chatToggle.querySelector('.chat-icon');
1671
  const closeIcon = chatToggle.querySelector('.close-icon');
 
1675
 
1676
  // Mode switching
1677
  modeButtons.forEach(btn => {
1678
+ btn.addEventListener('click', function () {
1679
  modeButtons.forEach(b => b.classList.remove('active'));
1680
  this.classList.add('active');
1681
  currentMode = this.getAttribute('data-mode');
1682
+
1683
  // Update placeholder based on mode
1684
  if (currentMode === 'general') {
1685
  chatInput.placeholder = 'Ask me anything...';
 
1693
 
1694
  // Quick actions
1695
  quickActionButtons.forEach(btn => {
1696
+ btn.addEventListener('click', function () {
1697
  const action = this.getAttribute('data-action');
1698
+
1699
  if (action === 'create-path') {
1700
  chatInput.value = 'I want to create a learning path for ';
1701
  chatInput.focus();
 
1713
  });
1714
 
1715
  // Auto-resize textarea
1716
+ chatInput.addEventListener('input', function () {
1717
  this.style.height = 'auto';
1718
  this.style.height = Math.min(this.scrollHeight, 120) + 'px';
1719
  });
1720
 
1721
  // Send message on Enter (Shift+Enter for new line)
1722
+ chatInput.addEventListener('keydown', function (e) {
1723
  if (e.key === 'Enter' && !e.shiftKey) {
1724
  e.preventDefault();
1725
  sendMessage();
 
1736
 
1737
  // Add user message to UI
1738
  addMessageToUI(message, 'user');
1739
+
1740
  // Clear input
1741
  if (!customMessage) {
1742
  chatInput.value = '';
 
1768
  if (data.success) {
1769
  const botMessage = data.response || data.data?.answer || 'I received your message!';
1770
  addMessageToUI(botMessage, 'bot');
1771
+
1772
  // Update conversation history
1773
  conversationHistory.push({
1774
  role: 'user',
 
1798
  function addMessageToUI(message, sender) {
1799
  const messageDiv = document.createElement('div');
1800
  messageDiv.className = `message ${sender}-message`;
1801
+
1802
+ const avatarSvg = sender === 'bot'
1803
  ? '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path></svg>'
1804
  : '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>';
1805
+
1806
  messageDiv.innerHTML = `
1807
  <div class="message-avatar">${avatarSvg}</div>
1808
  <div class="message-content">${formatMessage(message)}</div>
1809
  `;
1810
+
1811
  chatMessages.appendChild(messageDiv);
1812
  chatMessages.scrollTop = chatMessages.scrollHeight;
1813
  }
 
1825
  function showLoading(show) {
1826
  const sendIcon = chatSend.querySelector('.send-icon');
1827
  const loadingSpinner = chatSend.querySelector('.loading-spinner-chat');
1828
+
1829
  if (show) {
1830
  sendIcon.classList.add('hidden');
1831
  loadingSpinner.classList.remove('hidden');
 
1838
  }
1839
  });
1840
  </script>
1841
+ <script src="{{ url_for('static', filename='js/theme.js') }}"></script>
1842
+ {% if scroll_to_form %}
1843
+ <script>
1844
+ // Auto-scroll to the form when coming from /new-path route
1845
+ document.addEventListener('DOMContentLoaded', function () {
1846
+ const pathForm = document.getElementById('path-form');
1847
+ if (pathForm) {
1848
+ pathForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
1849
+ }
1850
+ });
1851
+ </script>
1852
+ {% endif %}
1853
  </body>
1854
+
1855
+ </html>