Edoruin commited on
Commit
fe6c501
·
1 Parent(s): 50a0ccf

update with teacher and students sides

Browse files
Dockerfile CHANGED
@@ -14,7 +14,10 @@ COPY app/ ./app/
14
  ENV FLASK_APP=app.app
15
  ENV FLASK_ENV=production
16
  ENV PORT=7860
 
17
 
18
  EXPOSE 7860
19
 
 
 
20
  CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "2", "--timeout", "120", "app.app:app"]
 
14
  ENV FLASK_APP=app.app
15
  ENV FLASK_ENV=production
16
  ENV PORT=7860
17
+ ENV DATA_DIR=/data
18
 
19
  EXPOSE 7860
20
 
21
+ VOLUME ["/data"]
22
+
23
  CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "2", "--timeout", "120", "app.app:app"]
app/app.py CHANGED
@@ -1,92 +1,154 @@
1
- from flask import Flask, render_template, jsonify, request
2
  from flask_cors import CORS
3
  import json
4
  import os
 
 
5
 
6
  app = Flask(__name__)
 
7
  CORS(app)
8
 
9
- # Data files
10
- DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
11
- COURSES_FILE = os.path.join(DATA_DIR, 'courses.json')
12
- CLASSROOMS_FILE = os.path.join(DATA_DIR, 'classrooms.json')
13
-
14
- os.makedirs(DATA_DIR, exist_ok=True)
15
-
16
- # Initialize data files if they don't exist
17
- if not os.path.exists(COURSES_FILE):
18
- with open(COURSES_FILE, 'w') as f:
19
- json.dump([
20
- {"id": 1, "title": "Introducción a la IA", "description": "Aprende los conceptos fundamentales de la Inteligencia Artificial", "category": "ai", "level": "beginner", "students": 45, "modules": 8, "icon": "🤖", "color": "linear-gradient(135deg, #4F46E5 0%, #7C3AED 100%)"},
21
- {"id": 2, "title": "Machine Learning Práctico", "description": "Implementa modelos de ML desde cero con Python", "category": "ml", "level": "intermediate", "students": 32, "modules": 12, "icon": "📊", "color": "linear-gradient(135deg, #10B981 0%, #34D399 100%)"},
22
- {"id": 3, "title": "Arduino para Principiantes", "description": "Crea tus primeros proyectos electrónicos con Arduino", "category": "arduino", "level": "beginner", "students": 56, "modules": 10, "icon": "⚡", "color": "linear-gradient(135deg, #00979D 0%, #4ECDC4 100%)"},
23
- {"id": 4, "title": "IoT con ESP32", "description": "Conecta dispositivos a internet y crea soluciones IoT", "category": "iot", "level": "intermediate", "students": 28, "modules": 8, "icon": "📡", "color": "linear-gradient(135deg, #F59E0B 0%, #FBBF24 100%)"},
24
- {"id": 5, "title": "Raspberry Pi Mastery", "description": "Domina Raspberry Pi para proyectos de automatización", "category": "raspberry", "level": "advanced", "students": 18, "modules": 15, "icon": "🖥️", "color": "linear-gradient(135deg, #C13584 0%, #E1306C 100%)"}
25
- ], f, indent=2)
26
-
27
- if not os.path.exists(CLASSROOMS_FILE):
28
- with open(CLASSROOMS_FILE, 'w') as f:
29
- json.dump([
30
- {"id": 1, "name": "IA Básico - Grupo A", "course_id": 1, "course_name": "Introducción a la IA", "students": 25, "capacity": 30, "schedule": "Lun-Mié 10:00", "tools": ["ai_test", "ml_classification"]},
31
- {"id": 2, "name": "ML Práctico - Grupo A", "course_id": 2, "course_name": "Machine Learning Práctico", "students": 20, "capacity": 25, "schedule": "Mar-Jue 14:00", "tools": ["ml_classification", "ml_regression"]},
32
- {"id": 3, "name": "Arduino - Taller", "course_id": 3, "course_name": "Arduino para Principiantes", "students": 18, "capacity": 20, "schedule": "Sáb 9:00", "tools": ["arduino_sim", "iot_sim"]}
33
- ], f, indent=2)
34
-
35
-
36
- def load_courses():
37
- with open(COURSES_FILE, 'r') as f:
38
- return json.load(f)
39
-
40
 
41
- def save_courses(courses):
42
- with open(COURSES_FILE, 'w') as f:
43
- json.dump(courses, f, indent=2)
 
44
 
45
-
46
- def load_classrooms():
47
- with open(CLASSROOMS_FILE, 'r') as f:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  return json.load(f)
49
 
 
 
 
50
 
51
- def save_classrooms(classrooms):
52
- with open(CLASSROOMS_FILE, 'w') as f:
53
- json.dump(classrooms, f, indent=2)
54
-
55
-
56
- # Routes
57
  @app.route('/')
58
  def index():
59
  return render_template('index.html')
60
 
61
-
62
- # API: Stats
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  @app.route('/api/stats')
64
  def get_stats():
65
- courses = load_courses()
66
- classrooms = load_classrooms()
67
- total_students = sum(c.get('students', 0) for c in courses)
 
68
 
 
 
 
 
 
 
 
69
  return jsonify({
70
  "courses": len(courses),
71
  "classrooms": len(classrooms),
72
  "students": total_students,
73
- "completionRate": 89
74
  })
75
 
76
-
77
- # API: Courses
78
  @app.route('/api/courses')
79
  def get_courses():
80
  limit = request.args.get('limit', type=int)
81
- courses = load_courses()
82
  if limit:
83
  courses = courses[:limit]
84
  return jsonify(courses)
85
 
86
-
87
  @app.route('/api/courses', methods=['POST'])
88
  def create_course():
89
- courses = load_courses()
90
  data = request.json
91
 
92
  icons = {"ai": "🤖", "ml": "📊", "arduino": "⚡", "iot": "📡", "raspberry": "🖥️", "python": "🐍", "data": "📈"}
@@ -105,20 +167,18 @@ def create_course():
105
  }
106
 
107
  courses.append(new_course)
108
- save_courses(courses)
109
  return jsonify(new_course), 201
110
 
111
-
112
- # API: Classrooms
113
  @app.route('/api/classrooms')
114
  def get_classrooms():
115
- return jsonify(load_classrooms())
116
-
117
 
118
  @app.route('/api/classrooms', methods=['POST'])
119
  def create_classroom():
120
- classrooms = load_classrooms()
121
- courses = load_courses()
122
  data = request.json
123
 
124
  course = next((c for c in courses if c['id'] == int(data.get('course_id', 0))), None)
@@ -132,22 +192,113 @@ def create_classroom():
132
  "capacity": data.get('capacity', 30),
133
  "students": 0,
134
  "schedule": "Por definir",
135
- "tools": data.get('tools', [])
 
136
  }
137
 
138
  classrooms.append(new_classroom)
139
- save_classrooms(classrooms)
140
  return jsonify(new_classroom), 201
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
- # API: Tools - configurable desde config.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  @app.route('/api/tools')
145
  def get_tools():
146
- from config import ToolConfig
147
  return jsonify(ToolConfig.get_tools_for_classroom())
148
 
149
-
150
- # API: Quiz
151
  @app.route('/api/quiz/questions')
152
  def get_quiz_questions():
153
  return jsonify([
@@ -159,8 +310,7 @@ def get_quiz_questions():
159
  {"question": "¿Cuál es la función de un sensor en un proyecto IoT?", "options": ["Almacenar datos", "Capturar información del entorno físico", "Procesar imágenes", "Conectar a internet"], "correct": 1}
160
  ])
161
 
162
-
163
- # API: Raspberry Guide
164
  @app.route('/api/raspberry/guide')
165
  def get_raspberry_guide():
166
  return jsonify([
@@ -171,6 +321,6 @@ def get_raspberry_guide():
171
  {"title": "Acceso Remoto", "description": "Configura SSH y VNC para acceder remotamente.", "code": "ssh pi@192.168.1.100\n# Contraseña: raspberry"}
172
  ])
173
 
174
-
175
  if __name__ == '__main__':
176
- app.run(host='0.0.0.0', port=7860, debug=True)
 
 
1
+ from flask import Flask, render_template, jsonify, request, session
2
  from flask_cors import CORS
3
  import json
4
  import os
5
+ import uuid
6
+ from datetime import datetime
7
 
8
  app = Flask(__name__)
9
+ app.secret_key = os.environ.get('SECRET_KEY', 'educateenIA_secret_key_2024')
10
  CORS(app)
11
 
12
+ # HF Spaces proporciona /data para almacenamiento persistente
13
+ DATA_DIR = os.environ.get('DATA_DIR', '/data')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ # Si no existe /data, usar directorio local (para desarrollo)
16
+ if not os.path.exists(DATA_DIR):
17
+ DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
18
+ os.makedirs(DATA_DIR, exist_ok=True)
19
 
20
+ COURSES_FILE = os.path.join(DATA_DIR, 'courses.json')
21
+ CLASSROOMS_FILE = os.path.join(DATA_DIR, 'classrooms.json')
22
+ USERS_FILE = os.path.join(DATA_DIR, 'users.json')
23
+ PROGRESS_FILE = os.path.join(DATA_DIR, 'progress.json')
24
+
25
+ def init_data_files():
26
+ if not os.path.exists(COURSES_FILE):
27
+ with open(COURSES_FILE, 'w') as f:
28
+ json.dump([
29
+ {"id": 1, "title": "Introducción a la IA", "description": "Aprende los conceptos fundamentales de la Inteligencia Artificial", "category": "ai", "level": "beginner", "students": 45, "modules": 8, "icon": "🤖", "color": "linear-gradient(135deg, #4F46E5 0%, #7C3AED 100%)"},
30
+ {"id": 2, "title": "Machine Learning Práctico", "description": "Implementa modelos de ML desde cero con Python", "category": "ml", "level": "intermediate", "students": 32, "modules": 12, "icon": "📊", "color": "linear-gradient(135deg, #10B981 0%, #34D399 100%)"},
31
+ {"id": 3, "title": "Arduino para Principiantes", "description": "Crea tus primeros proyectos electrónicos con Arduino", "category": "arduino", "level": "beginner", "students": 56, "modules": 10, "icon": "⚡", "color": "linear-gradient(135deg, #00979D 0%, #4ECDC4 100%)"},
32
+ {"id": 4, "title": "IoT con ESP32", "description": "Conecta dispositivos a internet y crea soluciones IoT", "category": "iot", "level": "intermediate", "students": 28, "modules": 8, "icon": "📡", "color": "linear-gradient(135deg, #F59E0B 0%, #FBBF24 100%)"},
33
+ {"id": 5, "title": "Raspberry Pi Mastery", "description": "Domina Raspberry Pi para proyectos de automatización", "category": "raspberry", "level": "advanced", "students": 18, "modules": 15, "icon": "🖥️", "color": "linear-gradient(135deg, #C13584 0%, #E1306C 100%)"}
34
+ ], f, indent=2)
35
+
36
+ if not os.path.exists(CLASSROOMS_FILE):
37
+ with open(CLASSROOMS_FILE, 'w') as f:
38
+ json.dump([
39
+ {"id": 1, "name": "IA Básico - Grupo A", "course_id": 1, "course_name": "Introducción a la IA", "students": 25, "capacity": 30, "schedule": "Lun-Mié 10:00", "tools": ["ai_test", "ml_classification"], "enrolled_students": []},
40
+ {"id": 2, "name": "ML Práctico - Grupo A", "course_id": 2, "course_name": "Machine Learning Práctico", "students": 20, "capacity": 25, "schedule": "Mar-Jue 14:00", "tools": ["ml_classification", "ml_regression"], "enrolled_students": []},
41
+ {"id": 3, "name": "Arduino - Taller", "course_id": 3, "course_name": "Arduino para Principiantes", "students": 18, "capacity": 20, "schedule": "Sáb 9:00", "tools": ["arduino_sim", "iot_sim"], "enrolled_students": []}
42
+ ], f, indent=2)
43
+
44
+ if not os.path.exists(USERS_FILE):
45
+ with open(USERS_FILE, 'w') as f:
46
+ json.dump({"students": [], "teachers": []}, f, indent=2)
47
+
48
+ if not os.path.exists(PROGRESS_FILE):
49
+ with open(PROGRESS_FILE, 'w') as f:
50
+ json.dump({}, f, indent=2)
51
+
52
+ init_data_files()
53
+
54
+ def load_json(filepath):
55
+ with open(filepath, 'r') as f:
56
  return json.load(f)
57
 
58
+ def save_json(filepath, data):
59
+ with open(filepath, 'w') as f:
60
+ json.dump(data, f, indent=2)
61
 
 
 
 
 
 
 
62
  @app.route('/')
63
  def index():
64
  return render_template('index.html')
65
 
66
+ # ============ AUTH ============
67
+ @app.route('/api/login', methods=['POST'])
68
+ def login():
69
+ data = request.json
70
+ role = data.get('role')
71
+ password = data.get('password')
72
+ name = data.get('name')
73
+ course_id = data.get('course_id')
74
+
75
+ users = load_json(USERS_FILE)
76
+
77
+ if role == 'teacher':
78
+ from app.config import AuthConfig
79
+ if password == AuthConfig.MASTER_PASSWORD:
80
+ session['user'] = {'role': 'teacher', 'name': 'Maestro'}
81
+ return jsonify({'success': True, 'role': 'teacher', 'name': 'Maestro'})
82
+ return jsonify({'success': False, 'error': 'Contraseña incorrecta'}), 401
83
+
84
+ elif role == 'student':
85
+ if not name or not course_id:
86
+ return jsonify({'success': False, 'error': 'Nombre y curso requeridos'}), 400
87
+
88
+ student_id = str(uuid.uuid4())[:8]
89
+ student = {'id': student_id, 'name': name, 'course_id': int(course_id), 'created_at': datetime.now().isoformat()}
90
+
91
+ users['students'].append(student)
92
+ save_json(USERS_FILE, users)
93
+
94
+ classrooms = load_json(CLASSROOMS_FILE)
95
+ for cr in classrooms:
96
+ if cr['course_id'] == int(course_id):
97
+ if 'enrolled_students' not in cr:
98
+ cr['enrolled_students'] = []
99
+ cr['enrolled_students'].append({'id': student_id, 'name': name})
100
+ save_json(CLASSROOMS_FILE, classrooms)
101
+
102
+ session['user'] = {'role': 'student', 'id': student_id, 'name': name, 'course_id': int(course_id)}
103
+ return jsonify({'success': True, 'role': 'student', 'name': name, 'student_id': student_id})
104
+
105
+ return jsonify({'success': False, 'error': 'Rol inválido'}), 400
106
+
107
+ @app.route('/api/logout', methods=['POST'])
108
+ def logout():
109
+ session.clear()
110
+ return jsonify({'success': True})
111
+
112
+ @app.route('/api/session')
113
+ def get_session():
114
+ if 'user' in session:
115
+ return jsonify({'authenticated': True, 'user': session['user']})
116
+ return jsonify({'authenticated': False})
117
+
118
+ # ============ STATS ============
119
  @app.route('/api/stats')
120
  def get_stats():
121
+ courses = load_json(COURSES_FILE)
122
+ classrooms = load_json(CLASSROOMS_FILE)
123
+ users = load_json(USERS_FILE)
124
+ progress = load_json(PROGRESS_FILE)
125
 
126
+ total_students = len(users.get('students', []))
127
+ completion_rate = 0
128
+ if progress:
129
+ completed = sum(1 for p in progress.values() if p.get('completed', False))
130
+ if completed > 0:
131
+ completion_rate = int((completed / len(progress)) * 100)
132
+
133
  return jsonify({
134
  "courses": len(courses),
135
  "classrooms": len(classrooms),
136
  "students": total_students,
137
+ "completionRate": completion_rate
138
  })
139
 
140
+ # ============ COURSES ============
 
141
  @app.route('/api/courses')
142
  def get_courses():
143
  limit = request.args.get('limit', type=int)
144
+ courses = load_json(COURSES_FILE)
145
  if limit:
146
  courses = courses[:limit]
147
  return jsonify(courses)
148
 
 
149
  @app.route('/api/courses', methods=['POST'])
150
  def create_course():
151
+ courses = load_json(COURSES_FILE)
152
  data = request.json
153
 
154
  icons = {"ai": "🤖", "ml": "📊", "arduino": "⚡", "iot": "📡", "raspberry": "🖥️", "python": "🐍", "data": "📈"}
 
167
  }
168
 
169
  courses.append(new_course)
170
+ save_json(COURSES_FILE, courses)
171
  return jsonify(new_course), 201
172
 
173
+ # ============ CLASSROOMS ============
 
174
  @app.route('/api/classrooms')
175
  def get_classrooms():
176
+ return jsonify(load_json(CLASSROOMS_FILE))
 
177
 
178
  @app.route('/api/classrooms', methods=['POST'])
179
  def create_classroom():
180
+ classrooms = load_json(CLASSROOMS_FILE)
181
+ courses = load_json(COURSES_FILE)
182
  data = request.json
183
 
184
  course = next((c for c in courses if c['id'] == int(data.get('course_id', 0))), None)
 
192
  "capacity": data.get('capacity', 30),
193
  "students": 0,
194
  "schedule": "Por definir",
195
+ "tools": data.get('tools', []),
196
+ "enrolled_students": []
197
  }
198
 
199
  classrooms.append(new_classroom)
200
+ save_json(CLASSROOMS_FILE, classrooms)
201
  return jsonify(new_classroom), 201
202
 
203
+ # ============ STUDENT PROGRESS ============
204
+ @app.route('/api/progress', methods=['GET'])
205
+ def get_progress():
206
+ student_id = request.args.get('student_id')
207
+ progress = load_json(PROGRESS_FILE)
208
+
209
+ if student_id:
210
+ return jsonify(progress.get(student_id, {}))
211
+ return jsonify(progress)
212
+
213
+ @app.route('/api/progress', methods=['POST'])
214
+ def update_progress():
215
+ data = request.json
216
+ student_id = data.get('student_id')
217
+ tool_id = data.get('tool_id')
218
+ score = data.get('score', 0)
219
+ completed = data.get('completed', False)
220
+ attempts = data.get('attempts', 1)
221
+
222
+ progress = load_json(PROGRESS_FILE)
223
+
224
+ if student_id not in progress:
225
+ progress[student_id] = {'name': data.get('name', ''), 'course_id': data.get('course_id'), 'activities': {}}
226
+
227
+ if tool_id not in progress[student_id]['activities']:
228
+ progress[student_id]['activities'][tool_id] = {'attempts': 0, 'best_score': 0, 'completed': False, 'extra_attempts': 0}
229
+
230
+ activity = progress[student_id]['activities'][tool_id]
231
+ activity['attempts'] = attempts
232
+ if score > activity['best_score']:
233
+ activity['best_score'] = score
234
+ if completed:
235
+ activity['completed'] = True
236
+
237
+ save_json(PROGRESS_FILE, progress)
238
+ return jsonify({'success': True})
239
+
240
+ @app.route('/api/progress/extra-attempts', methods=['POST'])
241
+ def add_extra_attempts():
242
+ data = request.json
243
+ student_id = data.get('student_id')
244
+ tool_id = data.get('tool_id')
245
+ extra = data.get('extra', 1)
246
+
247
+ progress = load_json(PROGRESS_FILE)
248
+
249
+ if student_id in progress and tool_id in progress[student_id]['activities']:
250
+ progress[student_id]['activities'][tool_id]['extra_attempts'] = progress[student_id]['activities'][tool_id].get('extra_attempts', 0) + extra
251
+ save_json(PROGRESS_FILE, progress)
252
+ return jsonify({'success': True, 'extra_attempts': progress[student_id]['activities'][tool_id]['extra_attempts']})
253
+
254
+ return jsonify({'success': False, 'error': 'Estudiante o actividad no encontrada'}), 404
255
 
256
+ @app.route('/api/students/progress')
257
+ def get_all_students_progress():
258
+ progress = load_json(PROGRESS_FILE)
259
+ users = load_json(USERS_FILE)
260
+ courses = load_json(COURSES_FILE)
261
+
262
+ result = []
263
+ for student_id, data in progress.items():
264
+ student = next((s for s in users['students'] if s['id'] == student_id), None)
265
+ course = next((c for c in courses if c['id'] == data.get('course_id')), None)
266
+
267
+ activities = []
268
+ total = 0
269
+ completed = 0
270
+ for tool_id, act in data.get('activities', {}).items():
271
+ total += 1
272
+ if act.get('completed'):
273
+ completed += 1
274
+ activities.append({
275
+ 'tool_id': tool_id,
276
+ 'attempts': act.get('attempts', 0),
277
+ 'extra_attempts': act.get('extra_attempts', 0),
278
+ 'best_score': act.get('best_score', 0),
279
+ 'completed': act.get('completed', False)
280
+ })
281
+
282
+ result.append({
283
+ 'id': student_id,
284
+ 'name': data.get('name', student_id),
285
+ 'course': course['title'] if course else 'Sin curso',
286
+ 'course_id': data.get('course_id'),
287
+ 'activities': activities,
288
+ 'total_activities': total,
289
+ 'completed_activities': completed,
290
+ 'progress_percent': int((completed / total) * 100) if total > 0 else 0
291
+ })
292
+
293
+ return jsonify(result)
294
+
295
+ # ============ TOOLS ============
296
  @app.route('/api/tools')
297
  def get_tools():
298
+ from app.config import ToolConfig
299
  return jsonify(ToolConfig.get_tools_for_classroom())
300
 
301
+ # ============ QUIZ ============
 
302
  @app.route('/api/quiz/questions')
303
  def get_quiz_questions():
304
  return jsonify([
 
310
  {"question": "¿Cuál es la función de un sensor en un proyecto IoT?", "options": ["Almacenar datos", "Capturar información del entorno físico", "Procesar imágenes", "Conectar a internet"], "correct": 1}
311
  ])
312
 
313
+ # ============ RASPBERRY GUIDE ============
 
314
  @app.route('/api/raspberry/guide')
315
  def get_raspberry_guide():
316
  return jsonify([
 
321
  {"title": "Acceso Remoto", "description": "Configura SSH y VNC para acceder remotamente.", "code": "ssh pi@192.168.1.100\n# Contraseña: raspberry"}
322
  ])
323
 
 
324
  if __name__ == '__main__':
325
+ port = int(os.environ.get('PORT', 7860))
326
+ app.run(host='0.0.0.0', port=port, debug=True)
app/config.py CHANGED
@@ -6,8 +6,6 @@ Agrega nuevas herramientas aquí sin modificar el código existente.
6
  class ToolConfig:
7
  """Configuración de herramientas disponibles en la plataforma."""
8
 
9
- # Registro centralizado de herramientas
10
- # Cada herramienta tiene: id, nombre, descripción, icono, tags, y página relacionada
11
  TOOLS = {
12
  "ai_test": {
13
  "id": "ai_test",
@@ -97,17 +95,14 @@ class ToolConfig:
97
 
98
  @classmethod
99
  def get_all_tools(cls):
100
- """Retorna todas las herramientas habilitadas."""
101
  return {k: v for k, v in cls.TOOLS.items() if v.get("enabled", True)}
102
 
103
  @classmethod
104
  def get_tool(cls, tool_id):
105
- """Retorna una herramienta específica por su ID."""
106
  return cls.TOOLS.get(tool_id)
107
 
108
  @classmethod
109
  def get_tools_for_classroom(cls):
110
- """Retorna herramientas formatadas para selector en creación de aulas."""
111
  tools = cls.get_all_tools()
112
  return [
113
  {
@@ -123,10 +118,6 @@ class ToolConfig:
123
 
124
  @classmethod
125
  def register_tool(cls, tool_id, name, description, icon, color, tags, page, **kwargs):
126
- """
127
- Método para registrar nuevas herramientas dinámicamente.
128
- Uso: ToolConfig.register_tool('nuevo_id', 'Nombre', 'Descripción', 'fa-icon', 'color', ['tags'], 'pagina')
129
- """
130
  cls.TOOLS[tool_id] = {
131
  "id": tool_id,
132
  "name": name,
@@ -141,8 +132,6 @@ class ToolConfig:
141
 
142
 
143
  class CategoryConfig:
144
- """Categorías de cursos."""
145
-
146
  CATEGORIES = {
147
  "ai": {"name": "Inteligencia Artificial", "icon": "fa-robot", "color": "#4F46E5"},
148
  "ml": {"name": "Machine Learning", "icon": "fa-brain", "color": "#10B981"},
@@ -160,3 +149,9 @@ class CategoryConfig:
160
  @classmethod
161
  def get(cls, key):
162
  return cls.CATEGORIES.get(key)
 
 
 
 
 
 
 
6
  class ToolConfig:
7
  """Configuración de herramientas disponibles en la plataforma."""
8
 
 
 
9
  TOOLS = {
10
  "ai_test": {
11
  "id": "ai_test",
 
95
 
96
  @classmethod
97
  def get_all_tools(cls):
 
98
  return {k: v for k, v in cls.TOOLS.items() if v.get("enabled", True)}
99
 
100
  @classmethod
101
  def get_tool(cls, tool_id):
 
102
  return cls.TOOLS.get(tool_id)
103
 
104
  @classmethod
105
  def get_tools_for_classroom(cls):
 
106
  tools = cls.get_all_tools()
107
  return [
108
  {
 
118
 
119
  @classmethod
120
  def register_tool(cls, tool_id, name, description, icon, color, tags, page, **kwargs):
 
 
 
 
121
  cls.TOOLS[tool_id] = {
122
  "id": tool_id,
123
  "name": name,
 
132
 
133
 
134
  class CategoryConfig:
 
 
135
  CATEGORIES = {
136
  "ai": {"name": "Inteligencia Artificial", "icon": "fa-robot", "color": "#4F46E5"},
137
  "ml": {"name": "Machine Learning", "icon": "fa-brain", "color": "#10B981"},
 
149
  @classmethod
150
  def get(cls, key):
151
  return cls.CATEGORIES.get(key)
152
+
153
+
154
+ # Configuración de autenticación
155
+ class AuthConfig:
156
+ MASTER_PASSWORD = "educateenIA2024"
157
+ SESSION_TIMEOUT = 3600
app/static/js/app.js CHANGED
@@ -1,16 +1,57 @@
1
  /**
2
  * EducateEnIA - Router Principal
3
- * Maneja la navegación entre páginas sin recargar
4
  */
5
 
6
  const App = {
7
  currentPage: 'dashboard',
8
- pages: ['dashboard', 'courses', 'classrooms', 'tools', 'ai-test', 'simulators', 'raspberry'],
 
9
 
10
  init() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  this.setupNavigation();
12
- this.setupToolCards();
13
- this.loadPage('dashboard');
 
14
  },
15
 
16
  setupNavigation() {
@@ -23,15 +64,6 @@ const App = {
23
  });
24
  },
25
 
26
- setupToolCards() {
27
- document.querySelectorAll('.tool-card').forEach(card => {
28
- card.addEventListener('click', () => {
29
- const tool = card.dataset.tool;
30
- this.openTool(tool);
31
- });
32
- });
33
- },
34
-
35
  navigateTo(page) {
36
  if (!this.pages.includes(page)) return;
37
 
@@ -47,12 +79,25 @@ const App = {
47
  this.currentPage = page;
48
  this.updatePageTitle(page);
49
 
50
- if (page === 'simulators') {
51
- setTimeout(() => Simulators.init(), 100);
52
- }
53
- if (page === 'raspberry') {
54
- setTimeout(() => Raspberry.init(), 100);
55
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  },
57
 
58
  openTool(tool) {
@@ -62,42 +107,20 @@ const App = {
62
  'ml-regression': 'simulators',
63
  'arduino-sim': 'simulators',
64
  'iot-sim': 'simulators',
65
- 'raspberry-learn': 'raspberry',
66
- 'python-basics': 'python-basics',
67
- 'data-science': 'data-science'
68
  };
69
 
70
  const page = toolPages[tool];
71
  this.navigateTo(page);
72
 
73
- if (page === 'ai-test') {
74
- setTimeout(() => Quiz.init(), 100);
75
- } else if (page === 'simulators' && tool !== 'ai-test') {
76
- const tabMap = {
77
- 'ml-classification': 'classification',
78
- 'ml-regression': 'regression',
79
- 'arduino-sim': 'arduino',
80
- 'iot-sim': 'iot'
81
- };
82
- setTimeout(() => {
83
- Simulators.switchTab(tabMap[tool] || 'classification');
84
- }, 150);
85
- }
86
  },
87
 
88
- updatePageTitle(page) {
89
- const titles = {
90
- dashboard: 'Dashboard',
91
- courses: 'Gestión de Cursos',
92
- classrooms: 'Aulas Virtuales',
93
- tools: 'Herramientas de Aprendizaje',
94
- 'ai-test': 'Test de Conocimientos IA',
95
- simulators: 'Simuladores Prácticos',
96
- raspberry: 'Aprende Raspberry Pi',
97
- 'python-basics': 'Python Básico',
98
- 'data-science': 'Ciencia de Datos'
99
- };
100
- document.getElementById('pageTitle').textContent = titles[page] || 'EducateEnIA';
101
  }
102
  };
103
 
 
1
  /**
2
  * EducateEnIA - Router Principal
 
3
  */
4
 
5
  const App = {
6
  currentPage: 'dashboard',
7
+ user: null,
8
+ pages: ['dashboard', 'courses', 'classrooms', 'tools', 'ai-test', 'simulators', 'raspberry', 'progress'],
9
 
10
  init() {
11
+ this.checkSession();
12
+ },
13
+
14
+ async checkSession() {
15
+ try {
16
+ const response = await fetch('/api/session');
17
+ const data = await response.json();
18
+ if (data.authenticated) {
19
+ this.user = data.user;
20
+ this.showMainApp();
21
+ } else {
22
+ this.showLogin();
23
+ }
24
+ } catch (error) {
25
+ console.error('Error checking session:', error);
26
+ this.showLogin();
27
+ }
28
+ },
29
+
30
+ showLogin() {
31
+ document.getElementById('loginScreen').style.display = 'flex';
32
+ document.getElementById('mainApp').style.display = 'none';
33
+ },
34
+
35
+ showMainApp() {
36
+ document.getElementById('loginScreen').style.display = 'none';
37
+ document.getElementById('mainApp').style.display = 'block';
38
+
39
+ if (this.user.role === 'teacher') {
40
+ document.getElementById('teacherNav').style.display = 'block';
41
+ document.getElementById('studentNav').style.display = 'none';
42
+ document.getElementById('newCourseBtn').style.display = 'inline-flex';
43
+ document.getElementById('newClassroomBtn').style.display = 'inline-flex';
44
+ } else {
45
+ document.getElementById('teacherNav').style.display = 'none';
46
+ document.getElementById('studentNav').style.display = 'block';
47
+ document.getElementById('newCourseBtn').style.display = 'none';
48
+ document.getElementById('newClassroomBtn').style.display = 'none';
49
+ }
50
+
51
  this.setupNavigation();
52
+ Dashboard.init();
53
+ Courses.init();
54
+ Tools.init();
55
  },
56
 
57
  setupNavigation() {
 
64
  });
65
  },
66
 
 
 
 
 
 
 
 
 
 
67
  navigateTo(page) {
68
  if (!this.pages.includes(page)) return;
69
 
 
79
  this.currentPage = page;
80
  this.updatePageTitle(page);
81
 
82
+ if (page === 'classrooms') Classrooms.init();
83
+ if (page === 'ai-test') Quiz.init();
84
+ if (page === 'simulators') Simulators.init();
85
+ if (page === 'raspberry') Raspberry.init();
86
+ if (page === 'progress' && this.user.role === 'teacher') TeacherProgress.init();
87
+ },
88
+
89
+ updatePageTitle(page) {
90
+ const titles = {
91
+ dashboard: 'Dashboard',
92
+ courses: 'Gestión de Cursos',
93
+ classrooms: 'Aulas Virtuales',
94
+ tools: 'Herramientas de Aprendizaje',
95
+ 'ai-test': 'Test de Conocimientos IA',
96
+ simulators: 'Simuladores Prácticos',
97
+ raspberry: 'Aprende Raspberry Pi',
98
+ progress: 'Progreso de Estudiantes'
99
+ };
100
+ document.getElementById('pageTitle').textContent = titles[page] || 'EducateEnIA';
101
  },
102
 
103
  openTool(tool) {
 
107
  'ml-regression': 'simulators',
108
  'arduino-sim': 'simulators',
109
  'iot-sim': 'simulators',
110
+ 'raspberry-learn': 'raspberry'
 
 
111
  };
112
 
113
  const page = toolPages[tool];
114
  this.navigateTo(page);
115
 
116
+ if (page === 'ai-test') setTimeout(() => Quiz.init(), 100);
117
+ if (page === 'simulators') setTimeout(() => Simulators.init(), 100);
 
 
 
 
 
 
 
 
 
 
 
118
  },
119
 
120
+ async logout() {
121
+ await fetch('/api/logout', { method: 'POST' });
122
+ this.user = null;
123
+ location.reload();
 
 
 
 
 
 
 
 
 
124
  }
125
  };
126
 
app/static/js/auth.js ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * EducateEnIA - Auth
3
+ * Manejo de autenticación
4
+ */
5
+
6
+ const Auth = {
7
+ currentRole: 'student',
8
+
9
+ init() {
10
+ this.loadCoursesForLogin();
11
+
12
+ document.querySelectorAll('.login-tab').forEach(tab => {
13
+ tab.addEventListener('click', () => {
14
+ this.selectRole(tab.dataset.role);
15
+ });
16
+ });
17
+ },
18
+
19
+ selectRole(role) {
20
+ this.currentRole = role;
21
+
22
+ document.querySelectorAll('.login-tab').forEach(t => t.classList.remove('active'));
23
+ document.querySelector(`.login-tab[data-role="${role}"]`).classList.add('active');
24
+
25
+ document.querySelectorAll('.login-form').forEach(f => f.classList.remove('active'));
26
+ document.getElementById(role + 'LoginForm').classList.add('active');
27
+ },
28
+
29
+ async loadCoursesForLogin() {
30
+ try {
31
+ const response = await fetch('/api/courses');
32
+ const courses = await response.json();
33
+ const select = document.getElementById('studentCourse');
34
+ if (select) {
35
+ select.innerHTML = '<option value="">-- Selecciona un curso --</option>' +
36
+ courses.map(c => `<option value="${c.id}">${c.title}</option>`).join('');
37
+ }
38
+ } catch (error) {
39
+ console.error('Error loading courses:', error);
40
+ }
41
+ },
42
+
43
+ async loginStudent(e) {
44
+ e.preventDefault();
45
+
46
+ const name = document.getElementById('studentName').value;
47
+ const courseId = document.getElementById('studentCourse').value;
48
+
49
+ if (!name || !courseId) {
50
+ this.showError('Por favor completa todos los campos');
51
+ return;
52
+ }
53
+
54
+ try {
55
+ const response = await fetch('/api/login', {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify({
59
+ role: 'student',
60
+ name: name,
61
+ course_id: courseId
62
+ })
63
+ });
64
+
65
+ const data = await response.json();
66
+
67
+ if (data.success) {
68
+ App.user = { role: 'student', id: data.student_id, name: data.name, course_id: parseInt(courseId) };
69
+ App.showMainApp();
70
+ } else {
71
+ this.showError(data.error);
72
+ }
73
+ } catch (error) {
74
+ this.showError('Error de conexión');
75
+ }
76
+ },
77
+
78
+ async loginTeacher(e) {
79
+ e.preventDefault();
80
+
81
+ const password = document.getElementById('teacherPassword').value;
82
+
83
+ try {
84
+ const response = await fetch('/api/login', {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({
88
+ role: 'teacher',
89
+ password: password
90
+ })
91
+ });
92
+
93
+ const data = await response.json();
94
+
95
+ if (data.success) {
96
+ App.user = { role: 'teacher', name: 'Maestro' };
97
+ App.showMainApp();
98
+ } else {
99
+ this.showError(data.error);
100
+ }
101
+ } catch (error) {
102
+ this.showError('Error de conexión');
103
+ }
104
+ },
105
+
106
+ showError(message) {
107
+ const errorEl = document.getElementById('loginError');
108
+ if (errorEl) {
109
+ errorEl.textContent = message;
110
+ errorEl.style.display = 'block';
111
+ setTimeout(() => {
112
+ errorEl.style.display = 'none';
113
+ }, 3000);
114
+ }
115
+ }
116
+ };
app/static/js/teacher.js ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * EducateEnIA - Teacher Progress
3
+ * Ver progreso de estudiantes
4
+ */
5
+
6
+ const TeacherProgress = {
7
+ students: [],
8
+
9
+ async init() {
10
+ await this.loadProgress();
11
+ },
12
+
13
+ async loadProgress() {
14
+ try {
15
+ const response = await fetch('/api/students/progress');
16
+ this.students = await response.json();
17
+ this.renderProgress();
18
+ } catch (error) {
19
+ console.error('Error loading progress:', error);
20
+ this.renderEmpty();
21
+ }
22
+ },
23
+
24
+ renderProgress() {
25
+ const container = document.getElementById('progressList');
26
+ if (!container) return;
27
+
28
+ if (!this.students || this.students.length === 0) {
29
+ container.innerHTML = '<p style="color: #6B7280; text-align: center;">No hay estudiantes registrados aún</p>';
30
+ return;
31
+ }
32
+
33
+ container.innerHTML = this.students.map(student => `
34
+ <div class="progress-card">
35
+ <div class="progress-header">
36
+ <div class="progress-student-info">
37
+ <div class="progress-avatar">${student.name.charAt(0).toUpperCase()}</div>
38
+ <div>
39
+ <div class="progress-name">${student.name}</div>
40
+ <div class="progress-course">${student.course}</div>
41
+ </div>
42
+ </div>
43
+ <div class="progress-bar-container">
44
+ <div class="progress-percent">${student.progress_percent}%</div>
45
+ <div class="progress-bar">
46
+ <div class="progress-fill" style="width: ${student.progress_percent}%"></div>
47
+ </div>
48
+ </div>
49
+ </div>
50
+ <div class="progress-activities">
51
+ ${student.activities.map(act => this.renderActivity(student.id, act)).join('')}
52
+ </div>
53
+ </div>
54
+ `).join('');
55
+ },
56
+
57
+ renderActivity(studentId, activity) {
58
+ const toolNames = {
59
+ 'ai_test': 'Test IA',
60
+ 'ml_classification': 'Clasificación',
61
+ 'ml_regression': 'Regresión',
62
+ 'arduino_sim': 'Arduino',
63
+ 'iot_sim': 'IoT',
64
+ 'raspberry_learn': 'Raspberry Pi'
65
+ };
66
+
67
+ const statusClass = activity.completed ? 'completed' : 'pending';
68
+ const statusText = activity.completed ? 'Completado' : 'Pendiente';
69
+ const maxAttempts = 1 + activity.extra_attempts;
70
+ const remaining = maxAttempts - activity.attempts;
71
+
72
+ return `
73
+ <div class="activity-item ${statusClass}">
74
+ <div class="activity-info">
75
+ <span class="activity-name">${toolNames[activity.tool_id] || activity.tool_id}</span>
76
+ <span class="activity-status">${statusText}</span>
77
+ </div>
78
+ <div class="activity-stats">
79
+ <span class="activity-score">Mejor: ${activity.best_score}%</span>
80
+ <span class="activity-attempts">Intentos: ${activity.attempts}/${maxAttempts}</span>
81
+ ${remaining > 0 && !activity.completed ? `<button class="btn-add-attempt" onclick="TeacherProgress.addAttempts('${studentId}', '${activity.tool_id}')">+1 Intento</button>` : ''}
82
+ </div>
83
+ </div>
84
+ `;
85
+ },
86
+
87
+ async addAttempts(studentId, toolId) {
88
+ try {
89
+ const response = await fetch('/api/progress/extra-attempts', {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json' },
92
+ body: JSON.stringify({ student_id: studentId, tool_id: toolId, extra: 1 })
93
+ });
94
+ if (response.ok) {
95
+ this.loadProgress();
96
+ }
97
+ } catch (error) {
98
+ console.error('Error adding attempts:', error);
99
+ }
100
+ },
101
+
102
+ renderEmpty() {
103
+ const container = document.getElementById('progressList');
104
+ if (container) {
105
+ container.innerHTML = '<p style="color: #6B7280; text-align: center;">No hay datos de progreso</p>';
106
+ }
107
+ }
108
+ };
app/templates/index.html CHANGED
@@ -8,33 +8,100 @@
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
  <style>
10
  :root {
11
- --primary: #4F46E5;
12
- --primary-dark: #3730A3;
13
- --primary-light: #818CF8;
14
- --secondary: #10B981;
15
- --secondary-dark: #059669;
16
- --accent: #F59E0B;
17
- --danger: #EF4444;
18
- --dark: #1F2937;
19
- --dark-secondary: #374151;
20
- --light: #F9FAFB;
21
- --light-secondary: #E5E7EB;
22
  --white: #FFFFFF;
23
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
24
  --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
25
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
26
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
27
  }
28
-
29
  * { margin: 0; padding: 0; box-sizing: border-box; }
30
  body { font-family: 'Inter', sans-serif; background: var(--light); color: var(--dark); line-height: 1.6; }
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  .sidebar {
33
  position: fixed; left: 0; top: 0; width: 260px; height: 100vh;
34
  background: linear-gradient(180deg, var(--dark) 0%, var(--dark-secondary) 100%);
35
  padding: 1.5rem; display: flex; flex-direction: column; z-index: 100; overflow-y: auto;
36
  }
37
-
38
  .logo { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem; margin-bottom: 2rem; }
39
  .logo-icon {
40
  width: 42px; height: 42px; background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
@@ -55,6 +122,15 @@
55
  .nav-item.active { background: var(--primary); color: white; }
56
  .nav-item i { width: 20px; text-align: center; }
57
 
 
 
 
 
 
 
 
 
 
58
  .main-content { margin-left: 260px; min-height: 100vh; }
59
 
60
  .header {
@@ -117,8 +193,6 @@
117
  .classroom-tools { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; }
118
  .tool-badge { padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.7rem; display: flex; align-items: center; gap: 0.25rem; }
119
  .no-tools { color: #9CA3AF; font-size: 0.8rem; }
120
- .classroom-meta { text-align: center; padding: 0 1rem; }
121
- .classroom-capacity { font-weight: 600; color: var(--primary); }
122
  .classroom-actions { display: flex; gap: 0.5rem; }
123
  .btn-icon { width: 36px; height: 36px; border-radius: 8px; border: none; background: white; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #6B7280; transition: all 0.2s; }
124
  .btn-icon:hover { background: var(--light-secondary); color: var(--dark); }
@@ -198,6 +272,30 @@
198
  .tool-selector-desc { display: block; font-size: 0.7rem; color: #6B7280; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
199
  .tool-selector-hint { font-size: 0.8rem; color: #9CA3AF; text-align: center; }
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  @media (max-width: 1024px) {
202
  .sidebar { width: 80px; padding: 1rem 0.5rem; }
203
  .logo-text, .nav-section-title, .nav-item span { display: none; }
@@ -209,133 +307,198 @@
209
  .main-content { margin-left: 0; }
210
  .stats-grid, .courses-grid, .tools-grid { grid-template-columns: 1fr; }
211
  .tool-selector-grid { grid-template-columns: 1fr; }
 
212
  }
213
  </style>
214
  </head>
215
  <body>
216
- <nav class="sidebar">
217
- <div class="logo">
218
- <div class="logo-icon"><i class="fas fa-brain"></i></div>
219
- <span class="logo-text">EducateEnIA</span>
220
- </div>
221
- <div class="nav-section">
222
- <div class="nav-section-title">Principal</div>
223
- <a class="nav-item active" data-page="dashboard"><i class="fas fa-th-large"></i><span>Dashboard</span></a>
224
- <a class="nav-item" data-page="courses"><i class="fas fa-book"></i><span>Cursos</span></a>
225
- <a class="nav-item" data-page="classrooms"><i class="fas fa-chalkboard-teacher"></i><span>Aulas</span></a>
226
- <a class="nav-item" data-page="tools"><i class="fas fa-tools"></i><span>Herramientas</span></a>
227
- </div>
228
- <div class="nav-section">
229
- <div class="nav-section-title">Aprendizaje</div>
230
- <a class="nav-item" data-page="ai-test"><i class="fas fa-robot"></i><span>Test de IA</span></a>
231
- <a class="nav-item" data-page="simulators"><i class="fas fa-microchip"></i><span>Simuladores</span></a>
232
- <a class="nav-item" data-page="raspberry"><i class="fas fa-server"></i><span>Raspberry Pi</span></a>
233
- </div>
234
- </nav>
235
-
236
- <main class="main-content">
237
- <header class="header">
238
- <h1 class="header-title" id="pageTitle">Dashboard</h1>
239
- <div class="header-actions">
240
- <button class="btn btn-secondary"><i class="fas fa-bell"></i></button>
241
- <button class="btn btn-primary" id="newCourseBtn"><i class="fas fa-plus"></i>Nuevo Curso</button>
242
  </div>
243
- </header>
244
-
245
- <div class="content-area">
246
- <!-- DASHBOARD -->
247
- <div class="page active" id="dashboard">
248
- <div class="stats-grid">
249
- <div class="stat-card"><div class="stat-icon blue"><i class="fas fa-book"></i></div><div class="stat-value">-</div><div class="stat-label">Cursos Activos</div></div>
250
- <div class="stat-card"><div class="stat-icon green"><i class="fas fa-chalkboard-teacher"></i></div><div class="stat-value">-</div><div class="stat-label">Aulas Virtuales</div></div>
251
- <div class="stat-card"><div class="stat-icon yellow"><i class="fas fa-users"></i></div><div class="stat-value">-</div><div class="stat-label">Estudiantes</div></div>
252
- <div class="stat-card"><div class="stat-icon purple"><i class="fas fa-certificate"></i></div><div class="stat-value">-</div><div class="stat-label">Tasa de Completado</div></div>
253
  </div>
254
- <div class="card">
255
- <div class="card-header"><h3 class="card-title">Cursos Recientes</h3><button class="btn btn-secondary" onclick="App.navigateTo('courses')">Ver Todos</button></div>
256
- <div class="card-body"><div class="courses-grid" id="recentCourses"></div></div>
257
  </div>
258
  </div>
259
 
260
- <!-- COURSES -->
261
- <div class="page" id="courses">
262
- <div class="courses-grid" id="allCourses"></div>
263
- </div>
264
-
265
- <!-- CLASSROOMS -->
266
- <div class="page" id="classrooms">
267
- <div class="card">
268
- <div class="card-header">
269
- <h3 class="card-title">Aulas Virtuales</h3>
270
- <button class="btn btn-primary" id="newClassroomBtn"><i class="fas fa-plus"></i>Nueva Aula</button>
271
- </div>
272
- <div class="card-body"><div class="classroom-list" id="classroomList"></div></div>
 
 
 
 
 
 
 
273
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  </div>
275
 
276
- <!-- TOOLS -->
277
- <div class="page" id="tools">
278
- <div class="tools-grid" id="toolsGrid"></div>
 
 
279
  </div>
280
 
281
- <!-- AI TEST -->
282
- <div class="page" id="ai-test">
283
- <div class="card">
284
- <div class="card-header"><h3 class="card-title">Test de Conocimientos Básicos en IA</h3></div>
285
- <div class="card-body"><div id="quizContainer"></div></div>
286
- </div>
287
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
- <!-- SIMULATORS -->
290
- <div class="page" id="simulators">
291
- <div class="sim-tabs">
292
- <div class="sim-tab-btn active" data-sim="classification">Clasificación</div>
293
- <div class="sim-tab-btn" data-sim="regression">Regresión</div>
294
- <div class="sim-tab-btn" data-sim="arduino">Arduino</div>
295
- <div class="sim-tab-btn" data-sim="iot">IoT</div>
296
  </div>
297
 
298
- <div id="simClassification" class="sim-tab-content active">
 
299
  <div class="card">
300
- <div class="card-header"><h3 class="card-title">Simulador de Clasificación</h3></div>
301
- <div class="card-body">
302
- <div class="simulator-controls">
303
- <div class="simulator-control"><label>Algoritmo</label><select class="form-select" id="classAlgorithm"><option>K-Nearest Neighbors</option><option>SVM</option><option>Decision Tree</option></select></div>
304
- <div class="simulator-control"><label>K (vecinos)</label><input type="range" id="kValue" min="1" max="15" value="5"><span id="kValueDisplay">5</span></div>
305
- <div class="simulator-control"><button class="btn btn-primary" id="trainClassification">Entrenar Modelo</button></div>
306
- <div class="simulator-control"><button class="btn btn-secondary" id="addClassPoint">Añadir Punto</button></div>
307
- </div>
308
- <div class="simulator-canvas" id="classificationCanvas"></div>
309
  </div>
 
310
  </div>
311
  </div>
312
 
313
- <div id="simRegression" class="sim-tab-content">
 
 
 
 
 
 
314
  <div class="card">
315
- <div class="card-header"><h3 class="card-title">Simulador de Regresión</h3></div>
316
- <div class="card-body">
317
- <div class="simulator-controls">
318
- <div class="simulator-control"><label>Tipo</label><select class="form-select" id="regType"><option>Lineal</option><option>Polinómica</option><option>Exponencial</option></select></div>
319
- <div class="simulator-control"><label>Grado</label><input type="range" id="polyDegree" min="2" max="5" value="2"><span id="polyDegreeDisplay">2</span></div>
320
- <div class="simulator-control"><button class="btn btn-primary" id="trainRegression">Ajustar Modelo</button></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  </div>
322
- <div class="simulator-canvas" id="regressionCanvas"></div>
323
  </div>
324
  </div>
325
- </div>
326
 
327
- <div id="simArduino" class="sim-tab-content">
328
- <div class="card">
329
- <div class="card-header"><h3 class="card-title">Simulador de Circuitos Arduino</h3></div>
330
- <div class="card-body">
331
- <div class="simulator-controls">
332
- <button class="btn btn-primary" id="addLed"><i class="fas fa-lightbulb"></i> LED</button>
333
- <button class="btn btn-secondary" id="addResistor"><i class="fas fa-atom"></i> Resistencia</button>
334
- <button class="btn btn-secondary" id="addButton"><i class="fas fa-hand-pointer"></i> Botón</button>
335
- <button class="btn btn-secondary" id="addSensor"><i class="fas fa-thermometer-half"></i> Sensor</button>
 
336
  </div>
337
- <div class="arduino-canvas" id="arduinoCanvas"></div>
338
- <div class="code-block" id="arduinoCode">// Código Arduino generado
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  void setup() {
340
  pinMode(13, OUTPUT);
341
  }
@@ -345,33 +508,42 @@ void loop() {
345
  digitalWrite(13, LOW);
346
  delay(1000);
347
  }</div>
 
348
  </div>
349
  </div>
350
- </div>
351
 
352
- <div id="simIot" class="sim-tab-content">
353
- <div class="card">
354
- <div class="card-header"><h3 class="card-title">Simulador IoT</h3></div>
355
- <div class="card-body">
356
- <div class="simulator-controls">
357
- <div class="simulator-control"><label>Dispositivo</label><select class="form-select" id="iotDevice"><option value="temp">Sensor Temperatura</option><option value="humidity">Sensor Humedad</option><option value="motion">Sensor Movimiento</option><option value="light">Sensor Luz</option></select></div>
358
- <div class="simulator-control"><button class="btn btn-primary" id="addIotDevice">Añadir Dispositivo</button></div>
 
 
359
  </div>
360
- <div class="simulator-canvas" id="iotCanvas" style="display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-start; padding: 1rem;"></div>
361
  </div>
362
  </div>
363
  </div>
364
- </div>
365
 
366
- <!-- RASPBERRY -->
367
- <div class="page" id="raspberry">
368
- <div class="card">
369
- <div class="card-header"><h3 class="card-title">Aprende Raspberry Pi</h3></div>
370
- <div class="card-body"><div class="raspberry-guide" id="raspberryGuide"></div></div>
 
 
 
 
 
 
 
 
 
371
  </div>
372
  </div>
373
- </div>
374
- </main>
375
 
376
  <!-- MODAL NUEVO CURSO -->
377
  <div class="modal" id="courseModal">
@@ -423,6 +595,7 @@ void loop() {
423
 
424
  <script src="/static/js/modal.js"></script>
425
  <script src="/static/js/app.js"></script>
 
426
  <script src="/static/js/dashboard.js"></script>
427
  <script src="/static/js/courses.js"></script>
428
  <script src="/static/js/classrooms.js"></script>
@@ -430,11 +603,10 @@ void loop() {
430
  <script src="/static/js/quiz.js"></script>
431
  <script src="/static/js/simulators.js"></script>
432
  <script src="/static/js/raspberry.js"></script>
 
433
  <script>
434
  document.addEventListener('DOMContentLoaded', () => {
435
- Dashboard.init();
436
- Courses.init();
437
- Tools.init();
438
 
439
  document.querySelectorAll('.nav-item').forEach(item => {
440
  item.addEventListener('click', () => {
@@ -444,6 +616,7 @@ void loop() {
444
  if (page === 'classrooms') Classrooms.init();
445
  if (page === 'ai-test') Quiz.init();
446
  if (page === 'raspberry') Raspberry.init();
 
447
  });
448
  });
449
 
 
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
  <style>
10
  :root {
11
+ --primary: #4F46E5; --primary-dark: #3730A3; --primary-light: #818CF8;
12
+ --secondary: #10B981; --secondary-dark: #059669;
13
+ --accent: #F59E0B; --danger: #EF4444;
14
+ --dark: #1F2937; --dark-secondary: #374151;
15
+ --light: #F9FAFB; --light-secondary: #E5E7EB;
 
 
 
 
 
 
16
  --white: #FFFFFF;
17
  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
18
  --shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
19
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
20
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
21
  }
 
22
  * { margin: 0; padding: 0; box-sizing: border-box; }
23
  body { font-family: 'Inter', sans-serif; background: var(--light); color: var(--dark); line-height: 1.6; }
24
 
25
+ /* Login Screen */
26
+ #loginScreen {
27
+ min-height: 100vh;
28
+ display: none;
29
+ align-items: center;
30
+ justify-content: center;
31
+ background: linear-gradient(135deg, #1F2937 0%, #4F46E5 100%);
32
+ padding: 1rem;
33
+ }
34
+ .login-container {
35
+ background: white;
36
+ border-radius: 20px;
37
+ padding: 2.5rem;
38
+ width: 100%;
39
+ max-width: 420px;
40
+ box-shadow: var(--shadow-xl);
41
+ }
42
+ .login-logo {
43
+ text-align: center;
44
+ margin-bottom: 2rem;
45
+ }
46
+ .login-logo-icon {
47
+ width: 70px; height: 70px;
48
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
49
+ border-radius: 16px;
50
+ display: inline-flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ font-size: 2rem;
54
+ color: white;
55
+ margin-bottom: 1rem;
56
+ }
57
+ .login-logo h1 { font-size: 1.5rem; color: var(--dark); margin-bottom: 0.25rem; }
58
+ .login-logo p { color: #6B7280; font-size: 0.9rem; }
59
+
60
+ .login-tabs { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; }
61
+ .login-tab {
62
+ flex: 1;
63
+ padding: 0.75rem;
64
+ border: 2px solid var(--light-secondary);
65
+ border-radius: 10px;
66
+ background: white;
67
+ cursor: pointer;
68
+ text-align: center;
69
+ transition: all 0.2s;
70
+ }
71
+ .login-tab:hover { border-color: var(--primary-light); }
72
+ .login-tab.active { border-color: var(--primary); background: #EEF2FF; }
73
+ .login-tab i { font-size: 1.5rem; margin-bottom: 0.5rem; display: block; }
74
+ .login-tab span { font-weight: 600; color: var(--dark); }
75
+
76
+ .login-form { display: none; }
77
+ .login-form.active { display: block; }
78
+
79
+ .login-input-group { margin-bottom: 1rem; }
80
+ .login-input-group label { display: block; font-size: 0.875rem; font-weight: 500; margin-bottom: 0.5rem; color: var(--dark); }
81
+ .login-input-group input, .login-input-group select {
82
+ width: 100%; padding: 0.75rem 1rem; border solid var(--light-secondary);
83
+ border-radius: 8px; font-size: 1px: 0.9rem; transition: all 0.2s;
84
+ }
85
+ .login-input-group input:focus, .login-input-group select:focus {
86
+ outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
87
+ }
88
+
89
+ .login-btn {
90
+ width: 100%; padding: 0.875rem; background: var(--primary); color: white;
91
+ border: none; border-radius: 8px; font-size: 1rem; font-weight: 600;
92
+ cursor: pointer; transition: all 0.2s; margin-top: 1rem;
93
+ }
94
+ .login-btn:hover { background: var(--primary-dark); }
95
+ .login-error { color: var(--danger); font-size: 0.875rem; margin-top: 0.5rem; text-align: center; display: none; }
96
+
97
+ /* Main App */
98
+ #mainApp { display: none; }
99
+
100
  .sidebar {
101
  position: fixed; left: 0; top: 0; width: 260px; height: 100vh;
102
  background: linear-gradient(180deg, var(--dark) 0%, var(--dark-secondary) 100%);
103
  padding: 1.5rem; display: flex; flex-direction: column; z-index: 100; overflow-y: auto;
104
  }
 
105
  .logo { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem; margin-bottom: 2rem; }
106
  .logo-icon {
107
  width: 42px; height: 42px; background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
 
122
  .nav-item.active { background: var(--primary); color: white; }
123
  .nav-item i { width: 20px; text-align: center; }
124
 
125
+ .user-info {
126
+ margin-top: auto; padding: 1rem; background: rgba(255,255,255,0.1);
127
+ border-radius: 10px; margin-top: 1rem;
128
+ }
129
+ .user-info .name { color: white; font-weight: 600; font-size: 0.9rem; }
130
+ .user-info .role { color: #9CA3AF; font-size: 0.75rem; }
131
+ .user-logout { margin-top: 0.75rem; width: 100%; padding: 0.5rem; background: transparent; border: 1px solid rgba(255,255,255,0.2); border-radius: 6px; color: #D1D5DB; cursor: pointer; font-size: 0.8rem; }
132
+ .user-logout:hover { background: rgba(255,255,255,0.1); color: white; }
133
+
134
  .main-content { margin-left: 260px; min-height: 100vh; }
135
 
136
  .header {
 
193
  .classroom-tools { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; }
194
  .tool-badge { padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.7rem; display: flex; align-items: center; gap: 0.25rem; }
195
  .no-tools { color: #9CA3AF; font-size: 0.8rem; }
 
 
196
  .classroom-actions { display: flex; gap: 0.5rem; }
197
  .btn-icon { width: 36px; height: 36px; border-radius: 8px; border: none; background: white; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #6B7280; transition: all 0.2s; }
198
  .btn-icon:hover { background: var(--light-secondary); color: var(--dark); }
 
272
  .tool-selector-desc { display: block; font-size: 0.7rem; color: #6B7280; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
273
  .tool-selector-hint { font-size: 0.8rem; color: #9CA3AF; text-align: center; }
274
 
275
+ /* Progress Styles */
276
+ .progress-card { background: white; border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; border: 1px solid var(--light-secondary); }
277
+ .progress-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
278
+ .progress-student-info { display: flex; align-items: center; gap: 1rem; }
279
+ .progress-avatar { width: 45px; height: 45px; background: var(--primary); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; font-weight: 600; }
280
+ .progress-name { font-weight: 600; color: var(--dark); }
281
+ .progress-course { font-size: 0.8rem; color: #6B7280; }
282
+ .progress-bar-container { width: 200px; text-align: right; }
283
+ .progress-percent { font-size: 1.25rem; font-weight: 700; color: var(--primary); margin-bottom: 0.25rem; }
284
+ .progress-bar { height: 8px; background: var(--light-secondary); border-radius: 4px; overflow: hidden; }
285
+ .progress-fill { height: 100%; background: var(--primary); border-radius: 4px; transition: width 0.3s; }
286
+ .progress-activities { display: flex; flex-direction: column; gap: 0.5rem; }
287
+ .activity-item { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; background: var(--light); border-radius: 8px; }
288
+ .activity-item.completed { background: #D1FAE5; }
289
+ .activity-info { display: flex; gap: 1rem; }
290
+ .activity-name { font-weight: 500; color: var(--dark); }
291
+ .activity-status { font-size: 0.75rem; padding: 0.25rem 0.5rem; border-radius: 4px; }
292
+ .activity-item.completed .activity-status { background: var(--secondary); color: white; }
293
+ .activity-item.pending .activity-status { background: #FEF3C7; color: var(--accent); }
294
+ .activity-stats { display: flex; align-items: center; gap: 1rem; }
295
+ .activity-score, .activity-attempts { font-size: 0.8rem; color: #6B7280; }
296
+ .btn-add-attempt { padding: 0.25rem 0.5rem; background: var(--primary); color: white; border: none; border-radius: 4px; font-size: 0.7rem; cursor: pointer; }
297
+ .btn-add-attempt:hover { background: var(--primary-dark); }
298
+
299
  @media (max-width: 1024px) {
300
  .sidebar { width: 80px; padding: 1rem 0.5rem; }
301
  .logo-text, .nav-section-title, .nav-item span { display: none; }
 
307
  .main-content { margin-left: 0; }
308
  .stats-grid, .courses-grid, .tools-grid { grid-template-columns: 1fr; }
309
  .tool-selector-grid { grid-template-columns: 1fr; }
310
+ .login-container { padding: 1.5rem; }
311
  }
312
  </style>
313
  </head>
314
  <body>
315
+ <!-- Login Screen -->
316
+ <div id="loginScreen">
317
+ <div class="login-container">
318
+ <div class="login-logo">
319
+ <div class="login-logo-icon"><i class="fas fa-brain"></i></div>
320
+ <h1>EducateEnIA</h1>
321
+ <p>Plataforma Educativa de Inteligencia Artificial</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  </div>
323
+
324
+ <div class="login-tabs">
325
+ <div class="login-tab active" data-role="student" onclick="Auth.selectRole('student')">
326
+ <i class="fas fa-user-graduate"></i>
327
+ <span>Estudiante</span>
 
 
 
 
 
328
  </div>
329
+ <div class="login-tab" data-role="teacher" onclick="Auth.selectRole('teacher')">
330
+ <i class="fas fa-chalkboard-teacher"></i>
331
+ <span>Maestro</span>
332
  </div>
333
  </div>
334
 
335
+ <!-- Student Form -->
336
+ <form id="studentLoginForm" class="login-form active" onsubmit="Auth.loginStudent(event)">
337
+ <div class="login-input-group">
338
+ <label>Nombre completo</label>
339
+ <input type="text" id="studentName" placeholder="Ingresa tu nombre" required>
340
+ </div>
341
+ <div class="login-input-group">
342
+ <label>Seleccionar Curso</label>
343
+ <select id="studentCourse" required>
344
+ <option value="">-- Selecciona un curso --</option>
345
+ </select>
346
+ </div>
347
+ <button type="submit" class="login-btn">Entrar al Aula</button>
348
+ </form>
349
+
350
+ <!-- Teacher Form -->
351
+ <form id="teacherLoginForm" class="login-form" onsubmit="Auth.loginTeacher(event)">
352
+ <div class="login-input-group">
353
+ <label>Contraseña del Maestro</label>
354
+ <input type="password" id="teacherPassword" placeholder="Ingresa la contraseña" required>
355
  </div>
356
+ <button type="submit" class="login-btn">Entrar como Maestro</button>
357
+ </form>
358
+
359
+ <div class="login-error" id="loginError"></div>
360
+ </div>
361
+ </div>
362
+
363
+ <!-- Main App -->
364
+ <div id="mainApp">
365
+ <nav class="sidebar">
366
+ <div class="logo">
367
+ <div class="logo-icon"><i class="fas fa-brain"></i></div>
368
+ <span class="logo-text">EducateEnIA</span>
369
+ </div>
370
+
371
+ <div class="nav-section">
372
+ <div class="nav-section-title">Principal</div>
373
+ <a class="nav-item active" data-page="dashboard"><i class="fas fa-th-large"></i><span>Dashboard</span></a>
374
+ <a class="nav-item" data-page="courses"><i class="fas fa-book"></i><span>Cursos</span></a>
375
+ <a class="nav-item" data-page="classrooms"><i class="fas fa-chalkboard-teacher"></i><span>Aulas</span></a>
376
+ <a class="nav-item" data-page="tools"><i class="fas fa-tools"></i><span>Herramientas</span></a>
377
+ </div>
378
+
379
+ <div class="nav-section" id="teacherNav" style="display: none;">
380
+ <div class="nav-section-title">Gestión</div>
381
+ <a class="nav-item" data-page="progress"><i class="fas fa-chart-bar"></i><span>Progreso</span></a>
382
  </div>
383
 
384
+ <div class="nav-section" id="studentNav" style="display: none;">
385
+ <div class="nav-section-title">Aprendizaje</div>
386
+ <a class="nav-item" data-page="ai-test"><i class="fas fa-robot"></i><span>Test de IA</span></a>
387
+ <a class="nav-item" data-page="simulators"><i class="fas fa-microchip"></i><span>Simuladores</span></a>
388
+ <a class="nav-item" data-page="raspberry"><i class="fas fa-server"></i><span>Raspberry Pi</span></a>
389
  </div>
390
 
391
+ <div class="user-info" id="userInfo">
392
+ <div class="name" id="userName">Usuario</div>
393
+ <div class="role" id="userRole">Rol</div>
394
+ <button class="user-logout" onclick="App.logout()"><i class="fas fa-sign-out-alt"></i> Cerrar Sesión</button>
 
 
395
  </div>
396
+ </nav>
397
+
398
+ <main class="main-content">
399
+ <header class="header">
400
+ <h1 class="header-title" id="pageTitle">Dashboard</h1>
401
+ <div class="header-actions">
402
+ <button class="btn btn-secondary"><i class="fas fa-bell"></i></button>
403
+ <button class="btn btn-primary" id="newCourseBtn"><i class="fas fa-plus"></i>Nuevo Curso</button>
404
+ <button class="btn btn-primary" id="newClassroomBtn"><i class="fas fa-plus"></i>Nueva Aula</button>
405
+ </div>
406
+ </header>
407
+
408
+ <div class="content-area">
409
+ <!-- DASHBOARD -->
410
+ <div class="page active" id="dashboard">
411
+ <div class="stats-grid">
412
+ <div class="stat-card"><div class="stat-icon blue"><i class="fas fa-book"></i></div><div class="stat-value">-</div><div class="stat-label">Cursos Activos</div></div>
413
+ <div class="stat-card"><div class="stat-icon green"><i class="fas fa-chalkboard-teacher"></i></div><div class="stat-value">-</div><div class="stat-label">Aulas Virtuales</div></div>
414
+ <div class="stat-card"><div class="stat-icon yellow"><i class="fas fa-users"></i></div><div class="stat-value">-</div><div class="stat-label">Estudiantes</div></div>
415
+ <div class="stat-card"><div class="stat-icon purple"><i class="fas fa-certificate"></i></div><div class="stat-value">-</div><div class="stat-label">Tasa de Completado</div></div>
416
+ </div>
417
+ <div class="card">
418
+ <div class="card-header"><h3 class="card-title">Cursos Recientes</h3><button class="btn btn-secondary" onclick="App.navigateTo('courses')">Ver Todos</button></div>
419
+ <div class="card-body"><div class="courses-grid" id="recentCourses"></div></div>
420
+ </div>
421
+ </div>
422
 
423
+ <!-- COURSES -->
424
+ <div class="page" id="courses">
425
+ <div class="courses-grid" id="allCourses"></div>
 
 
 
 
426
  </div>
427
 
428
+ <!-- CLASSROOMS -->
429
+ <div class="page" id="classrooms">
430
  <div class="card">
431
+ <div class="card-header">
432
+ <h3 class="card-title">Aulas Virtuales</h3>
433
+ <button class="btn btn-primary" id="newClassroomBtn2"><i class="fas fa-plus"></i>Nueva Aula</button>
 
 
 
 
 
 
434
  </div>
435
+ <div class="card-body"><div class="classroom-list" id="classroomList"></div></div>
436
  </div>
437
  </div>
438
 
439
+ <!-- TOOLS -->
440
+ <div class="page" id="tools">
441
+ <div class="tools-grid" id="toolsGrid"></div>
442
+ </div>
443
+
444
+ <!-- AI TEST -->
445
+ <div class="page" id="ai-test">
446
  <div class="card">
447
+ <div class="card-header"><h3 class="card-title">Test de Conocimientos Básicos en IA</h3></div>
448
+ <div class="card-body"><div id="quizContainer"></div></div>
449
+ </div>
450
+ </div>
451
+
452
+ <!-- SIMULATORS -->
453
+ <div class="page" id="simulators">
454
+ <div class="sim-tabs">
455
+ <div class="sim-tab-btn active" data-sim="classification">Clasificación</div>
456
+ <div class="sim-tab-btn" data-sim="regression">Regresión</div>
457
+ <div class="sim-tab-btn" data-sim="arduino">Arduino</div>
458
+ <div class="sim-tab-btn" data-sim="iot">IoT</div>
459
+ </div>
460
+
461
+ <div id="simClassification" class="sim-tab-content active">
462
+ <div class="card">
463
+ <div class="card-header"><h3 class="card-title">Simulador de Clasificación</h3></div>
464
+ <div class="card-body">
465
+ <div class="simulator-controls">
466
+ <div class="simulator-control"><label>Algoritmo</label><select class="form-select" id="classAlgorithm"><option>K-Nearest Neighbors</option><option>SVM</option><option>Decision Tree</option></select></div>
467
+ <div class="simulator-control"><label>K (vecinos)</label><input type="range" id="kValue" min="1" max="15" value="5"><span id="kValueDisplay">5</span></div>
468
+ <div class="simulator-control"><button class="btn btn-primary" id="trainClassification">Entrenar Modelo</button></div>
469
+ <div class="simulator-control"><button class="btn btn-secondary" id="addClassPoint">Añadir Punto</button></div>
470
+ </div>
471
+ <div class="simulator-canvas" id="classificationCanvas"></div>
472
  </div>
 
473
  </div>
474
  </div>
 
475
 
476
+ <div id="simRegression" class="sim-tab-content">
477
+ <div class="card">
478
+ <div class="card-header"><h3 class="card-title">Simulador de Regresión</h3></div>
479
+ <div class="card-body">
480
+ <div class="simulator-controls">
481
+ <div class="simulator-control"><label>Tipo</label><select class="form-select" id="regType"><option>Lineal</option><option>Polinómica</option><option>Exponencial</option></select></div>
482
+ <div class="simulator-control"><label>Grado</label><input type="range" id="polyDegree" min="2" max="5" value="2"><span id="polyDegreeDisplay">2</span></div>
483
+ <div class="simulator-control"><button class="btn btn-primary" id="trainRegression">Ajustar Modelo</button></div>
484
+ </div>
485
+ <div class="simulator-canvas" id="regressionCanvas"></div>
486
  </div>
487
+ </div>
488
+ </div>
489
+
490
+ <div id="simArduino" class="sim-tab-content">
491
+ <div class="card">
492
+ <div class="card-header"><h3 class="card-title">Simulador de Circuitos Arduino</h3></div>
493
+ <div class="card-body">
494
+ <div class="simulator-controls">
495
+ <button class="btn btn-primary" id="addLed"><i class="fas fa-lightbulb"></i> LED</button>
496
+ <button class="btn btn-secondary" id="addResistor"><i class="fas fa-atom"></i> Resistencia</button>
497
+ <button class="btn btn-secondary" id="addButton"><i class="fas fa-hand-pointer"></i> Botón</button>
498
+ <button class="btn btn-secondary" id="addSensor"><i class="fas fa-thermometer-half"></i> Sensor</button>
499
+ </div>
500
+ <div class="arduino-canvas" id="arduinoCanvas"></div>
501
+ <div class="code-block" id="arduinoCode">// Código Arduino generado
502
  void setup() {
503
  pinMode(13, OUTPUT);
504
  }
 
508
  digitalWrite(13, LOW);
509
  delay(1000);
510
  }</div>
511
+ </div>
512
  </div>
513
  </div>
 
514
 
515
+ <div id="simIot" class="sim-tab-content">
516
+ <div class="card">
517
+ <div class="card-header"><h3 class="card-title">Simulador IoT</h3></div>
518
+ <div class="card-body">
519
+ <div class="simulator-controls">
520
+ <div class="simulator-control"><label>Dispositivo</label><select class="form-select" id="iotDevice"><option value="temp">Sensor Temperatura</option><option value="humidity">Sensor Humedad</option><option value="motion">Sensor Movimiento</option><option value="light">Sensor Luz</option></select></div>
521
+ <div class="simulator-control"><button class="btn btn-primary" id="addIotDevice">Añadir Dispositivo</button></div>
522
+ </div>
523
+ <div class="simulator-canvas" id="iotCanvas" style="display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-start; padding: 1rem;"></div>
524
  </div>
 
525
  </div>
526
  </div>
527
  </div>
 
528
 
529
+ <!-- RASPBERRY -->
530
+ <div class="page" id="raspberry">
531
+ <div class="card">
532
+ <div class="card-header"><h3 class="card-title">Aprende Raspberry Pi</h3></div>
533
+ <div class="card-body"><div class="raspberry-guide" id="raspberryGuide"></div></div>
534
+ </div>
535
+ </div>
536
+
537
+ <!-- PROGRESS (Teacher Only) -->
538
+ <div class="page" id="progress">
539
+ <div class="card">
540
+ <div class="card-header"><h3 class="card-title">Progreso de Estudiantes</h3></div>
541
+ <div class="card-body" id="progressList"></div>
542
+ </div>
543
  </div>
544
  </div>
545
+ </main>
546
+ </div>
547
 
548
  <!-- MODAL NUEVO CURSO -->
549
  <div class="modal" id="courseModal">
 
595
 
596
  <script src="/static/js/modal.js"></script>
597
  <script src="/static/js/app.js"></script>
598
+ <script src="/static/js/auth.js"></script>
599
  <script src="/static/js/dashboard.js"></script>
600
  <script src="/static/js/courses.js"></script>
601
  <script src="/static/js/classrooms.js"></script>
 
603
  <script src="/static/js/quiz.js"></script>
604
  <script src="/static/js/simulators.js"></script>
605
  <script src="/static/js/raspberry.js"></script>
606
+ <script src="/static/js/teacher.js"></script>
607
  <script>
608
  document.addEventListener('DOMContentLoaded', () => {
609
+ Auth.init();
 
 
610
 
611
  document.querySelectorAll('.nav-item').forEach(item => {
612
  item.addEventListener('click', () => {
 
616
  if (page === 'classrooms') Classrooms.init();
617
  if (page === 'ai-test') Quiz.init();
618
  if (page === 'raspberry') Raspberry.init();
619
+ if (page === 'progress') TeacherProgress.init();
620
  });
621
  });
622