Edoruin commited on
Commit
cbcf0cd
1 Parent(s): f82f2b6

solving persistence secrets and a iacre and mor

Browse files
app/app.py CHANGED
@@ -3,8 +3,14 @@ from flask_cors import CORS
3
  import os
4
  import uuid
5
  import json
 
6
  from datetime import datetime
7
  from datasets import Dataset, load_dataset
 
 
 
 
 
8
 
9
  app = Flask(__name__)
10
  app.secret_key = os.environ.get('SECRET_KEY', 'educateenIA_secret_key_2024')
@@ -16,6 +22,56 @@ if not os.path.exists(DATA_DIR):
16
  os.makedirs(DATA_DIR, exist_ok=True)
17
 
18
  REPO_ID = os.environ.get('REPO_ID', None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  def load_from_disk(filename, fallback=None):
21
  filepath = os.path.join(DATA_DIR, f"{filename}.json")
@@ -34,10 +90,16 @@ COURSES_DATA = []
34
 
35
  CLASSROOMS_DATA = []
36
 
37
- courses_ds = load_from_disk("courses", COURSES_DATA)
38
- classrooms_ds = load_from_disk("classrooms", CLASSROOMS_DATA)
39
- users_ds = load_from_disk("users", [])
40
- progress_ds = load_from_disk("progress", [])
 
 
 
 
 
 
41
 
42
  def get_courses_list():
43
  return courses_ds.to_list()
@@ -45,7 +107,7 @@ def get_courses_list():
45
  def save_courses_list(data):
46
  global courses_ds
47
  courses_ds = Dataset.from_list(data)
48
- save_to_disk(courses_ds, "courses")
49
 
50
  def get_classrooms_list():
51
  return classrooms_ds.to_list()
@@ -53,7 +115,7 @@ def get_classrooms_list():
53
  def save_classrooms_list(data):
54
  global classrooms_ds
55
  classrooms_ds = Dataset.from_list(data)
56
- save_to_disk(classrooms_ds, "classrooms")
57
 
58
  def get_users_list():
59
  return users_ds.to_list()
@@ -61,7 +123,7 @@ def get_users_list():
61
  def save_users_list(data):
62
  global users_ds
63
  users_ds = Dataset.from_list(data)
64
- save_to_disk(users_ds, "users")
65
 
66
  def get_progress_list():
67
  return progress_ds.to_list()
@@ -69,7 +131,23 @@ def get_progress_list():
69
  def save_progress_list(data):
70
  global progress_ds
71
  progress_ds = Dataset.from_list(data)
72
- save_to_disk(progress_ds, "progress")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  @app.route('/')
75
  def index():
@@ -425,6 +503,179 @@ def get_raspberry_guide():
425
  {"title": "Acceso Remoto", "description": "Configura SSH y VNC para acceder remotamente.", "code": "ssh pi@192.168.1.100\n# Contrase帽a: raspberry"}
426
  ])
427
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  if __name__ == '__main__':
429
  port = int(os.environ.get('PORT', 7860))
430
  app.run(host='0.0.0.0', port=port, debug=True)
 
3
  import os
4
  import uuid
5
  import json
6
+ import requests
7
  from datetime import datetime
8
  from datasets import Dataset, load_dataset
9
+ from huggingface_hub import HfApi, login
10
+ import logging
11
+
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
 
15
  app = Flask(__name__)
16
  app.secret_key = os.environ.get('SECRET_KEY', 'educateenIA_secret_key_2024')
 
22
  os.makedirs(DATA_DIR, exist_ok=True)
23
 
24
  REPO_ID = os.environ.get('REPO_ID', None)
25
+ HF_TOKEN = os.environ.get('HF_TOKEN', None)
26
+
27
+ def get_hf_api():
28
+ if HF_TOKEN:
29
+ try:
30
+ login(token=HF_TOKEN)
31
+ return HfApi(token=HF_TOKEN)
32
+ except Exception as e:
33
+ logger.warning(f"Error al iniciar sesi贸n en HF: {e}")
34
+ return None
35
+
36
+ def load_from_hub(filename, fallback=None):
37
+ if not REPO_ID or not HF_TOKEN:
38
+ return load_from_disk(filename, fallback)
39
+
40
+ try:
41
+ api = get_hf_api()
42
+ if api:
43
+ url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/{filename}.json"
44
+ response = requests.get(url, headers={"Authorization": f"Bearer {HF_TOKEN}"})
45
+ if response.status_code == 200:
46
+ data = response.json()
47
+ return Dataset.from_list(data)
48
+ except Exception as e:
49
+ logger.warning(f"Error al cargar {filename} desde HF: {e}")
50
+
51
+ return load_from_disk(filename, fallback)
52
+
53
+ def save_to_hub(ds, filename):
54
+ if not REPO_ID or not HF_TOKEN:
55
+ save_to_disk(ds, filename)
56
+ return
57
+
58
+ try:
59
+ api = get_hf_api()
60
+ if api:
61
+ data = ds.to_list()
62
+ json_str = json.dumps(data, indent=2)
63
+ api.upload_file(
64
+ path_or_fileobj=json_str.encode(),
65
+ path_in_repo=f"{filename}.json",
66
+ repo_id=REPO_ID,
67
+ repo_type="dataset",
68
+ commit_message=f"Update {filename}"
69
+ )
70
+ logger.info(f"Guardado {filename} en HF Hub")
71
+ except Exception as e:
72
+ logger.warning(f"Error al guardar {filename} en HF: {e}")
73
+
74
+ save_to_disk(ds, filename)
75
 
76
  def load_from_disk(filename, fallback=None):
77
  filepath = os.path.join(DATA_DIR, f"{filename}.json")
 
90
 
91
  CLASSROOMS_DATA = []
92
 
93
+ courses_ds = load_from_hub("courses", COURSES_DATA)
94
+ classrooms_ds = load_from_hub("classrooms", CLASSROOMS_DATA)
95
+ users_ds = load_from_hub("users", [])
96
+ progress_ds = load_from_hub("progress", [])
97
+
98
+ QUIZZES_DATA = []
99
+ QUIZ_RESPONSES_DATA = []
100
+
101
+ quizzes_ds = load_from_hub("quizzes", QUIZZES_DATA)
102
+ quiz_responses_ds = load_from_hub("quiz_responses", QUIZ_RESPONSES_DATA)
103
 
104
  def get_courses_list():
105
  return courses_ds.to_list()
 
107
  def save_courses_list(data):
108
  global courses_ds
109
  courses_ds = Dataset.from_list(data)
110
+ save_to_hub(courses_ds, "courses")
111
 
112
  def get_classrooms_list():
113
  return classrooms_ds.to_list()
 
115
  def save_classrooms_list(data):
116
  global classrooms_ds
117
  classrooms_ds = Dataset.from_list(data)
118
+ save_to_hub(classrooms_ds, "classrooms")
119
 
120
  def get_users_list():
121
  return users_ds.to_list()
 
123
  def save_users_list(data):
124
  global users_ds
125
  users_ds = Dataset.from_list(data)
126
+ save_to_hub(users_ds, "users")
127
 
128
  def get_progress_list():
129
  return progress_ds.to_list()
 
131
  def save_progress_list(data):
132
  global progress_ds
133
  progress_ds = Dataset.from_list(data)
134
+ save_to_hub(progress_ds, "progress")
135
+
136
+ def get_quizzes_list():
137
+ return quizzes_ds.to_list()
138
+
139
+ def save_quizzes_list(data):
140
+ global quizzes_ds
141
+ quizzes_ds = Dataset.from_list(data)
142
+ save_to_hub(quizzes_ds, "quizzes")
143
+
144
+ def get_quiz_responses_list():
145
+ return quiz_responses_ds.to_list()
146
+
147
+ def save_quiz_responses_list(data):
148
+ global quiz_responses_ds
149
+ quiz_responses_ds = Dataset.from_list(data)
150
+ save_to_hub(quiz_responses_ds, "quiz_responses")
151
 
152
  @app.route('/')
153
  def index():
 
503
  {"title": "Acceso Remoto", "description": "Configura SSH y VNC para acceder remotamente.", "code": "ssh pi@192.168.1.100\n# Contrase帽a: raspberry"}
504
  ])
505
 
506
+ @app.route('/api/quizzes', methods=['GET'])
507
+ def get_quizzes_api():
508
+ classroom_id = request.args.get('classroom_id', type=int)
509
+ quizzes = get_quizzes_list()
510
+
511
+ if classroom_id:
512
+ quizzes = [q for q in quizzes if q.get('classroom_id') == classroom_id]
513
+
514
+ return jsonify(quizzes)
515
+
516
+ @app.route('/api/quizzes', methods=['POST'])
517
+ def create_quiz():
518
+ data = request.json
519
+
520
+ quizzes = get_quizzes_list()
521
+ new_id = max([q['id'] for q in quizzes], default=0) + 1
522
+
523
+ new_quiz = {
524
+ "id": new_id,
525
+ "title": data.get('title'),
526
+ "description": data.get('description', ''),
527
+ "classroom_id": int(data.get('classroom_id')),
528
+ "questions": json.dumps(data.get('questions', [])),
529
+ "active": False,
530
+ "created_at": datetime.now().isoformat()
531
+ }
532
+
533
+ quizzes.append(new_quiz)
534
+ save_quizzes_list(quizzes)
535
+
536
+ return jsonify(new_quiz), 201
537
+
538
+ @app.route('/api/quizzes/<int:quiz_id>', methods=['PUT'])
539
+ def update_quiz(quiz_id):
540
+ data = request.json
541
+ quizzes = get_quizzes_list()
542
+
543
+ for quiz in quizzes:
544
+ if quiz['id'] == quiz_id:
545
+ if 'title' in data:
546
+ quiz['title'] = data['title']
547
+ if 'description' in data:
548
+ quiz['description'] = data['description']
549
+ if 'questions' in data:
550
+ quiz['questions'] = json.dumps(data['questions'])
551
+ if 'active' in data:
552
+ quiz['active'] = data['active']
553
+ break
554
+
555
+ save_quizzes_list(quizzes)
556
+ return jsonify({'success': True})
557
+
558
+ @app.route('/api/quizzes/<int:quiz_id>', methods=['DELETE'])
559
+ def delete_quiz(quiz_id):
560
+ quizzes = get_quizzes_list()
561
+ quizzes = [q for q in quizzes if q['id'] != quiz_id]
562
+ save_quizzes_list(quizzes)
563
+
564
+ responses = get_quiz_responses_list()
565
+ responses = [r for r in responses if r.get('quiz_id') != quiz_id]
566
+ save_quiz_responses_list(responses)
567
+
568
+ return jsonify({'success': True})
569
+
570
+ @app.route('/api/quizzes/<int:quiz_id>/toggle', methods=['POST'])
571
+ def toggle_quiz(quiz_id):
572
+ quizzes = get_quizzes_list()
573
+
574
+ for quiz in quizzes:
575
+ if quiz['id'] == quiz_id:
576
+ quiz['active'] = not quiz.get('active', False)
577
+ break
578
+
579
+ save_quizzes_list(quizzes)
580
+ return jsonify({'success': True})
581
+
582
+ @app.route('/api/student/quizzes')
583
+ def get_student_quizzes():
584
+ student_id = request.args.get('student_id')
585
+ course_id = request.args.get('course_id', type=int)
586
+
587
+ classrooms = get_classrooms_list()
588
+ classroom = next((c for c in classrooms if c.get('course_id') == course_id), None)
589
+
590
+ if not classroom:
591
+ return jsonify([])
592
+
593
+ quizzes = get_quizzes_list()
594
+ classroom_quizzes = [q for q in quizzes if q.get('classroom_id') == classroom['id']]
595
+
596
+ responses = get_quiz_responses_list()
597
+
598
+ result = []
599
+ for quiz in classroom_quizzes:
600
+ student_response = next((r for r in responses if r.get('quiz_id') == quiz['id'] and r.get('student_id') == student_id), None)
601
+
602
+ result.append({
603
+ 'id': quiz['id'],
604
+ 'title': quiz.get('title'),
605
+ 'description': quiz.get('description'),
606
+ 'active': quiz.get('active', False),
607
+ 'questions': json.loads(quiz.get('questions', '[]')),
608
+ 'completed': student_response is not None,
609
+ 'score': student_response.get('score') if student_response else None
610
+ })
611
+
612
+ return jsonify(result)
613
+
614
+ @app.route('/api/quizzes/<int:quiz_id>/submit', methods=['POST'])
615
+ def submit_quiz(quiz_id):
616
+ data = request.json
617
+ student_id = data.get('student_id')
618
+ answers = data.get('answers', [])
619
+
620
+ quizzes = get_quizzes_list()
621
+ quiz = next((q for q in quizzes if q['id'] == quiz_id), None)
622
+
623
+ if not quiz:
624
+ return jsonify({'error': 'Cuestionario no encontrado'}), 404
625
+
626
+ questions = json.loads(quiz.get('questions', '[]'))
627
+
628
+ score = 0
629
+ total = len(questions)
630
+ for i, question in enumerate(questions):
631
+ correct_idx = question.get('correct', -1)
632
+ if i < len(answers) and answers[i] == correct_idx:
633
+ score += 1
634
+
635
+ responses = get_quiz_responses_list()
636
+
637
+ existing = next((r for r in responses if r.get('quiz_id') == quiz_id and r.get('student_id') == student_id), None)
638
+
639
+ if existing:
640
+ existing['answers'] = json.dumps(answers)
641
+ existing['score'] = score
642
+ existing['total'] = total
643
+ existing['submitted_at'] = datetime.now().isoformat()
644
+ else:
645
+ responses.append({
646
+ 'quiz_id': quiz_id,
647
+ 'student_id': student_id,
648
+ 'answers': json.dumps(answers),
649
+ 'score': score,
650
+ 'total': total,
651
+ 'submitted_at': datetime.now().isoformat()
652
+ })
653
+
654
+ save_quiz_responses_list(responses)
655
+
656
+ return jsonify({'success': True, 'score': score, 'total': total})
657
+
658
+ @app.route('/api/quizzes/<int:quiz_id>/responses')
659
+ def get_quiz_responses(quiz_id):
660
+ responses = get_quiz_responses_list()
661
+ quizzes = get_quizzes_list()
662
+ users = get_users_list()
663
+
664
+ quiz_responses = [r for r in responses if r.get('quiz_id') == quiz_id]
665
+
666
+ result = []
667
+ for r in quiz_responses:
668
+ student = next((u for u in users if u.get('id') == r.get('student_id')), None)
669
+ result.append({
670
+ 'student_name': student.get('name') if student else 'Desconocido',
671
+ 'student_id': r.get('student_id'),
672
+ 'score': r.get('score', 0),
673
+ 'total': r.get('total', 0),
674
+ 'submitted_at': r.get('submitted_at')
675
+ })
676
+
677
+ return jsonify(result)
678
+
679
  if __name__ == '__main__':
680
  port = int(os.environ.get('PORT', 7860))
681
  app.run(host='0.0.0.0', port=port, debug=True)
app/static/js/app.js CHANGED
@@ -5,7 +5,7 @@
5
  const App = {
6
  currentPage: 'dashboard',
7
  user: null,
8
- pages: ['dashboard', 'courses', 'classrooms', 'tools', 'ai-test', 'simulators', 'raspberry', 'progress', 'my-progress'],
9
 
10
  init() {
11
  this.checkSession();
 
5
  const App = {
6
  currentPage: 'dashboard',
7
  user: null,
8
+ pages: ['dashboard', 'courses', 'classrooms', 'tools', 'ai-test', 'simulators', 'raspberry', 'progress', 'my-progress', 'quizzes', 'quizzes-student'],
9
 
10
  init() {
11
  this.checkSession();
app/static/js/auth.js CHANGED
@@ -5,6 +5,7 @@
5
 
6
  const Auth = {
7
  currentRole: 'student',
 
8
 
9
  init() {
10
  this.loadCoursesForLogin();
@@ -16,6 +17,10 @@ const Auth = {
16
  });
17
  },
18
 
 
 
 
 
19
  selectRole(role) {
20
  this.currentRole = role;
21
 
 
5
 
6
  const Auth = {
7
  currentRole: 'student',
8
+ user: null,
9
 
10
  init() {
11
  this.loadCoursesForLogin();
 
17
  });
18
  },
19
 
20
+ getUser() {
21
+ return this.user || App.user;
22
+ },
23
+
24
  selectRole(role) {
25
  this.currentRole = role;
26
 
app/static/js/student_quizzes.js ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const StudentQuizzes = {
2
+ quizzes: [],
3
+ currentQuiz: null,
4
+ answers: [],
5
+
6
+ async init() {
7
+ const user = Auth.getUser();
8
+ if (!user || user.role !== 'student') return;
9
+
10
+ await this.loadQuizzes();
11
+ },
12
+
13
+ async loadQuizzes() {
14
+ const user = Auth.getUser();
15
+ if (!user) return;
16
+
17
+ try {
18
+ const response = await fetch(`/api/student/quizzes?student_id=${user.id}&course_id=${user.course_id}`);
19
+ this.quizzes = await response.json();
20
+ this.renderQuizzes();
21
+ } catch (error) {
22
+ console.error('Error loading quizzes:', error);
23
+ }
24
+ },
25
+
26
+ renderQuizzes() {
27
+ const container = document.getElementById('studentQuizzesList');
28
+ if (!container) return;
29
+
30
+ if (this.quizzes.length === 0) {
31
+ container.innerHTML = '<p style="color: #6B7280; text-align: center;">No hay cuestionarios disponibles para tu curso</p>';
32
+ return;
33
+ }
34
+
35
+ container.innerHTML = this.quizzes.map(quiz => `
36
+ <div class="student-quiz-card">
37
+ <div class="student-quiz-header">
38
+ <h3 class="student-quiz-title">${quiz.title}</h3>
39
+ <span class="student-quiz-status ${quiz.active ? (quiz.completed ? 'completed' : 'pending') : 'pending'}">
40
+ ${!quiz.active ? 'No disponible' : (quiz.completed ? 'Completado' : 'Pendiente')}
41
+ </span>
42
+ </div>
43
+ <p style="color: #6B7280; margin-bottom: 1rem;">${quiz.description || 'Sin descripci贸n'}</p>
44
+ ${quiz.completed ? `
45
+ <div style="text-align: center; padding: 1rem; background: var(--light); border-radius: 8px;">
46
+ <div class="student-quiz-score">${quiz.score}/${quiz.questions.length}</div>
47
+ <p style="color: #6B7280; font-size: 0.875rem;">Puntuaci贸n obtained</p>
48
+ </div>
49
+ ` : quiz.active ? `
50
+ <button class="btn btn-primary" onclick="StudentQuizzes.openQuiz(${quiz.id})">
51
+ <i class="fas fa-play"></i> Responder Cuestionario
52
+ </button>
53
+ ` : `
54
+ <p style="color: #9CA3AF; font-size: 0.875rem;">Espera a que el maestro active el cuestionario</p>
55
+ `}
56
+ </div>
57
+ `).join('');
58
+ },
59
+
60
+ async openQuiz(quizId) {
61
+ const quiz = this.quizzes.find(q => q.id === quizId);
62
+ if (!quiz || !quiz.active) {
63
+ alert('Este cuestionario no est谩 disponible');
64
+ return;
65
+ }
66
+
67
+ this.currentQuiz = quiz;
68
+ this.answers = new Array(quiz.questions.length).fill(-1);
69
+
70
+ document.getElementById('quizAnswerTitle').textContent = quiz.title;
71
+
72
+ const content = document.getElementById('quizAnswerContent');
73
+ content.innerHTML = quiz.questions.map((q, idx) => `
74
+ <div class="question-item">
75
+ <h4>Pregunta ${idx + 1}: ${q.question}</h4>
76
+ <div class="options-container">
77
+ ${q.options.map((opt, optIdx) => `
78
+ <div class="option-row">
79
+ <input type="radio" name="question_${idx}" value="${optIdx}" id="q${idx}_opt${optIdx}"
80
+ onchange="StudentQuizzes.setAnswer(${idx}, ${optIdx})">
81
+ <label for="q${idx}_opt${optIdx}" style="flex: 1; cursor: pointer;">${opt}</label>
82
+ </div>
83
+ `).join('')}
84
+ </div>
85
+ </div>
86
+ `).join('');
87
+
88
+ content.innerHTML += `
89
+ <div class="form-actions">
90
+ <button class="btn btn-secondary" onclick="StudentQuizzes.closeQuizModal()">Cancelar</button>
91
+ <button class="btn btn-primary" onclick="StudentQuizzes.submitQuiz()">Enviar Respuestas</button>
92
+ </div>
93
+ `;
94
+
95
+ document.getElementById('quizAnswerModal').style.display = 'block';
96
+ },
97
+
98
+ setAnswer(questionIdx, answerIdx) {
99
+ this.answers[questionIdx] = answerIdx;
100
+ },
101
+
102
+ async submitQuiz() {
103
+ if (this.answers.includes(-1)) {
104
+ alert('Por favor responde todas las preguntas');
105
+ return;
106
+ }
107
+
108
+ const user = Auth.getUser();
109
+ if (!user) return;
110
+
111
+ try {
112
+ const response = await fetch(`/api/quizzes/${this.currentQuiz.id}/submit`, {
113
+ method: 'POST',
114
+ headers: { 'Content-Type': 'application/json' },
115
+ body: JSON.stringify({
116
+ student_id: user.id,
117
+ answers: this.answers
118
+ })
119
+ });
120
+
121
+ const result = await response.json();
122
+
123
+ if (result.success) {
124
+ alert(`隆Cuestionario enviado!\nPuntuaci贸n: ${result.score}/${result.total}`);
125
+ this.closeQuizModal();
126
+ this.loadQuizzes();
127
+ }
128
+ } catch (error) {
129
+ console.error('Error submitting quiz:', error);
130
+ alert('Error al enviar el cuestionario');
131
+ }
132
+ },
133
+
134
+ closeQuizModal() {
135
+ document.getElementById('quizAnswerModal').style.display = 'none';
136
+ this.currentQuiz = null;
137
+ this.answers = [];
138
+ }
139
+ };
app/static/js/teacher.js CHANGED
@@ -6,6 +6,7 @@
6
  const TeacherProgress = {
7
  students: [],
8
  allStudents: [],
 
9
 
10
  async init() {
11
  await this.loadProgress();
@@ -173,3 +174,220 @@ const TeacherProgress = {
173
  }
174
  }
175
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  const TeacherProgress = {
7
  students: [],
8
  allStudents: [],
9
+ classrooms: [],
10
 
11
  async init() {
12
  await this.loadProgress();
 
174
  }
175
  }
176
  };
177
+
178
+ const QuizManager = {
179
+ quizzes: [],
180
+ classrooms: [],
181
+ currentEdit: null,
182
+
183
+ async init() {
184
+ await this.loadClassrooms();
185
+ await this.loadQuizzes();
186
+ },
187
+
188
+ async loadClassrooms() {
189
+ try {
190
+ const response = await fetch('/api/classrooms');
191
+ this.classrooms = await response.json();
192
+ } catch (error) {
193
+ console.error('Error loading classrooms:', error);
194
+ this.classrooms = [];
195
+ }
196
+ },
197
+
198
+ async loadQuizzes() {
199
+ try {
200
+ const response = await fetch('/api/quizzes');
201
+ this.quizzes = await response.json();
202
+ this.renderQuizzes();
203
+ } catch (error) {
204
+ console.error('Error loading quizzes:', error);
205
+ }
206
+ },
207
+
208
+ renderQuizzes() {
209
+ const container = document.getElementById('quizzesList');
210
+ if (!container) return;
211
+
212
+ if (this.quizzes.length === 0) {
213
+ container.innerHTML = '<p style="color: #6B7280; text-align: center;">No hay cuestionarios creados</p>';
214
+ return;
215
+ }
216
+
217
+ container.innerHTML = this.quizzes.map(quiz => {
218
+ const classroom = this.classrooms.find(c => c.id === quiz.classroom_id);
219
+ const questions = JSON.parse(quiz.questions || '[]');
220
+ return `
221
+ <div class="quiz-card">
222
+ <div class="quiz-header">
223
+ <h3>${quiz.title}</h3>
224
+ <span class="quiz-badge ${quiz.active ? 'active' : 'inactive'}">
225
+ ${quiz.active ? 'Activo' : 'Inactivo'}
226
+ </span>
227
+ </div>
228
+ <p class="quiz-description">${quiz.description || 'Sin descripci贸n'}</p>
229
+ <p class="quiz-meta">Aula: ${classroom ? classroom.name : 'Sin aula'} | Preguntas: ${questions.length}</p>
230
+ <div class="quiz-actions">
231
+ <button class="btn btn-primary" onclick="QuizManager.toggleQuiz(${quiz.id})">
232
+ ${quiz.active ? 'Desactivar' : 'Activar'}
233
+ </button>
234
+ <button class="btn btn-info" onclick="QuizManager.viewResponses(${quiz.id})">
235
+ Ver Respuestas
236
+ </button>
237
+ <button class="btn btn-danger" onclick="QuizManager.deleteQuiz(${quiz.id})">
238
+ Eliminar
239
+ </button>
240
+ </div>
241
+ </div>
242
+ `;
243
+ }).join('');
244
+ },
245
+
246
+ async createQuiz() {
247
+ const title = document.getElementById('quizTitle').value.trim();
248
+ const description = document.getElementById('quizDescription').value.trim();
249
+ const classroomId = document.getElementById('quizClassroom').value;
250
+ const questionsContainer = document.getElementById('questionsList');
251
+
252
+ if (!title || !classroomId) {
253
+ alert('Por favor completa el t铆tulo y selecciona un aula');
254
+ return;
255
+ }
256
+
257
+ const questions = [];
258
+ const questionElements = questionsContainer.querySelectorAll('.question-item');
259
+
260
+ for (let i = 0; i < questionElements.length; i++) {
261
+ const qEl = questionElements[i];
262
+ const questionText = qEl.querySelector('.question-text').value.trim();
263
+ const options = [
264
+ qEl.querySelector('.option-0').value.trim(),
265
+ qEl.querySelector('.option-1').value.trim(),
266
+ qEl.querySelector('.option-2').value.trim(),
267
+ qEl.querySelector('.option-3').value.trim()
268
+ ];
269
+ const correct = parseInt(qEl.querySelector('.correct-select').value);
270
+
271
+ if (!questionText || options.some(o => !o)) {
272
+ alert(`Por favor completa la pregunta ${i + 1}`);
273
+ return;
274
+ }
275
+
276
+ questions.push({ question: questionText, options, correct });
277
+ }
278
+
279
+ if (questions.length === 0) {
280
+ alert('Agrega al menos una pregunta');
281
+ return;
282
+ }
283
+
284
+ try {
285
+ const response = await fetch('/api/quizzes', {
286
+ method: 'POST',
287
+ headers: { 'Content-Type': 'application/json' },
288
+ body: JSON.stringify({
289
+ title,
290
+ description,
291
+ classroom_id: parseInt(classroomId),
292
+ questions
293
+ })
294
+ });
295
+
296
+ if (response.ok) {
297
+ alert('Cuestionario creado exitosamente');
298
+ this.loadQuizzes();
299
+ this.closeModal();
300
+ }
301
+ } catch (error) {
302
+ console.error('Error creating quiz:', error);
303
+ alert('Error al crear cuestionario');
304
+ }
305
+ },
306
+
307
+ addQuestionField() {
308
+ const container = document.getElementById('questionsList');
309
+ const questionNum = container.children.length + 1;
310
+
311
+ const html = `
312
+ <div class="question-item">
313
+ <h4>Pregunta ${questionNum}</h4>
314
+ <input type="text" class="question-text" placeholder="Escribe la pregunta" style="width: 100%; padding: 8px; margin-bottom: 10px;">
315
+ <div class="options-container">
316
+ ${[0,1,2,3].map(i => `
317
+ <div class="option-row">
318
+ <input type="radio" name="correct-${questionNum}" value="${i}" ${i === 0 ? 'checked' : ''}>
319
+ <input type="text" class="option-${i}" placeholder="Opci贸n ${i + 1}" style="flex: 1; padding: 8px;">
320
+ </div>
321
+ `).join('')}
322
+ </div>
323
+ <select class="correct-select" style="margin-top: 10px;">
324
+ ${[0,1,2,3].map(i => `<option value="${i}">Respuesta correcta: Opci贸n ${i + 1}</option>`).join('')}
325
+ </select>
326
+ <button class="btn btn-danger btn-sm" onclick="this.parentElement.remove()" style="margin-top: 10px;">Eliminar pregunta</button>
327
+ </div>
328
+ `;
329
+
330
+ container.insertAdjacentHTML('beforeend', html);
331
+ },
332
+
333
+ async toggleQuiz(quizId) {
334
+ try {
335
+ await fetch(`/api/quizzes/${quizId}/toggle`, { method: 'POST' });
336
+ this.loadQuizzes();
337
+ } catch (error) {
338
+ console.error('Error toggling quiz:', error);
339
+ }
340
+ },
341
+
342
+ async deleteQuiz(quizId) {
343
+ if (!confirm('驴Est谩s seguro de eliminar este cuestionario?')) return;
344
+
345
+ try {
346
+ await fetch(`/api/quizzes/${quizId}`, { method: 'DELETE' });
347
+ this.loadQuizzes();
348
+ } catch (error) {
349
+ console.error('Error deleting quiz:', error);
350
+ }
351
+ },
352
+
353
+ async viewResponses(quizId) {
354
+ try {
355
+ const response = await fetch(`/api/quizzes/${quizId}/responses`);
356
+ const responses = await response.json();
357
+
358
+ if (responses.length === 0) {
359
+ alert('No hay respuestas a煤n');
360
+ return;
361
+ }
362
+
363
+ const quiz = this.quizzes.find(q => q.id === quizId);
364
+ let message = `Respuestas de: ${quiz.title}\n\n`;
365
+
366
+ responses.forEach(r => {
367
+ message += `${r.student_name}: ${r.score}/${r.total} puntos\n`;
368
+ });
369
+
370
+ alert(message);
371
+ } catch (error) {
372
+ console.error('Error loading responses:', error);
373
+ }
374
+ },
375
+
376
+ openModal() {
377
+ document.getElementById('quizModal').style.display = 'block';
378
+ document.getElementById('quizTitle').value = '';
379
+ document.getElementById('quizDescription').value = '';
380
+ document.getElementById('quizClassroom').value = '';
381
+ document.getElementById('questionsList').innerHTML = '';
382
+
383
+ const classroomSelect = document.getElementById('quizClassroom');
384
+ classroomSelect.innerHTML = '<option value="">Selecciona un aula</option>' +
385
+ this.classrooms.map(c => `<option value="${c.id}">${c.name}</option>`).join('');
386
+
387
+ this.addQuestionField();
388
+ },
389
+
390
+ closeModal() {
391
+ document.getElementById('quizModal').style.display = 'none';
392
+ }
393
+ };
app/templates/index.html CHANGED
@@ -319,6 +319,35 @@
319
  .my-activity-score { font-size: 0.875rem; color: var(--dark); }
320
  .my-activity-attempts { font-size: 0.75rem; color: #6B7280; }
321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  @media (max-width: 1024px) {
323
  .sidebar { width: 80px; padding: 1rem 0.5rem; }
324
  .logo-text, .nav-section-title, .nav-item span { display: none; }
@@ -402,11 +431,13 @@
402
  <div class="nav-section" id="teacherNav" style="display: none;">
403
  <div class="nav-section-title">Gesti贸n</div>
404
  <a class="nav-item" data-page="progress"><i class="fas fa-chart-bar"></i><span>Progreso</span></a>
 
405
  </div>
406
 
407
  <div class="nav-section" id="studentNav" style="display: none;">
408
  <div class="nav-section-title">Aprendizaje</div>
409
  <a class="nav-item" data-page="ai-test"><i class="fas fa-robot"></i><span>Test de IA</span></a>
 
410
  <a class="nav-item" data-page="simulators"><i class="fas fa-microchip"></i><span>Simuladores</span></a>
411
  <a class="nav-item" data-page="raspberry"><i class="fas fa-server"></i><span>Raspberry Pi</span></a>
412
  <a class="nav-item" data-page="my-progress"><i class="fas fa-chart-line"></i><span>Mi Progreso</span></a>
@@ -473,6 +504,14 @@
473
  </div>
474
  </div>
475
 
 
 
 
 
 
 
 
 
476
  <!-- SIMULATORS -->
477
  <div class="page" id="simulators">
478
  <div class="sim-tabs">
@@ -566,6 +605,17 @@ void loop() {
566
  </div>
567
  </div>
568
 
 
 
 
 
 
 
 
 
 
 
 
569
  <!-- STUDENT PROGRESS -->
570
  <div class="page" id="my-progress">
571
  <div class="card">
@@ -625,6 +675,52 @@ void loop() {
625
  </div>
626
  </div>
627
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
628
  <script src="/static/js/modal.js"></script>
629
  <script src="/static/js/app.js"></script>
630
  <script src="/static/js/auth.js"></script>
@@ -637,6 +733,7 @@ void loop() {
637
  <script src="/static/js/raspberry.js"></script>
638
  <script src="/static/js/teacher.js"></script>
639
  <script src="/static/js/student_progress.js"></script>
 
640
  <script>
641
  document.addEventListener('DOMContentLoaded', () => {
642
  Auth.init();
@@ -651,6 +748,8 @@ void loop() {
651
  if (page === 'raspberry') Raspberry.init();
652
  if (page === 'progress') TeacherProgress.init();
653
  if (page === 'my-progress') StudentProgress.init();
 
 
654
  });
655
  });
656
 
 
319
  .my-activity-score { font-size: 0.875rem; color: var(--dark); }
320
  .my-activity-attempts { font-size: 0.75rem; color: #6B7280; }
321
 
322
+ .quiz-card { background: white; border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; border: 1px solid var(--light-secondary); }
323
+ .quiz-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; }
324
+ .quiz-header h3 { margin: 0; color: var(--dark); }
325
+ .quiz-badge { padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.75rem; font-weight: 600; }
326
+ .quiz-badge.active { background: #D1FAE5; color: var(--secondary); }
327
+ .quiz-badge.inactive { background: #FEF3C7; color: var(--accent); }
328
+ .quiz-description { color: #6B7280; font-size: 0.9rem; margin-bottom: 0.5rem; }
329
+ .quiz-meta { font-size: 0.8rem; color: #9CA3AF; margin-bottom: 1rem; }
330
+ .quiz-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
331
+ .btn-danger { background: var(--danger); color: white; }
332
+ .btn-danger:hover { background: #DC2626; }
333
+ .btn-info { background: #3B82F6; color: white; }
334
+ .btn-info:hover { background: #2563EB; }
335
+ .btn-sm { padding: 0.375rem 0.75rem; font-size: 0.8rem; }
336
+
337
+ .student-quiz-card { background: white; border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; border: 1px solid var(--light-secondary); }
338
+ .student-quiz-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; }
339
+ .student-quiz-title { font-size: 1.1rem; font-weight: 600; color: var(--dark); margin: 0; }
340
+ .student-quiz-status { padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.75rem; font-weight: 600; }
341
+ .student-quiz-status.pending { background: #FEF3C7; color: var(--accent); }
342
+ .student-quiz-status.completed { background: #D1FAE5; color: var(--secondary); }
343
+ .student-quiz-score { font-size: 1.5rem; font-weight: 700; color: var(--primary); }
344
+
345
+ .question-item { background: var(--light); padding: 1rem; border-radius: 8px; margin-bottom: 1rem; }
346
+ .question-item h4 { margin: 0 0 0.75rem; color: var(--dark); }
347
+ .options-container { display: flex; flex-direction: column; gap: 0.5rem; }
348
+ .option-row { display: flex; align-items: center; gap: 0.5rem; }
349
+ .option-row input[type="radio"] { width: auto; }
350
+
351
  @media (max-width: 1024px) {
352
  .sidebar { width: 80px; padding: 1rem 0.5rem; }
353
  .logo-text, .nav-section-title, .nav-item span { display: none; }
 
431
  <div class="nav-section" id="teacherNav" style="display: none;">
432
  <div class="nav-section-title">Gesti贸n</div>
433
  <a class="nav-item" data-page="progress"><i class="fas fa-chart-bar"></i><span>Progreso</span></a>
434
+ <a class="nav-item" data-page="quizzes"><i class="fas fa-clipboard-list"></i><span>Cuestionarios</span></a>
435
  </div>
436
 
437
  <div class="nav-section" id="studentNav" style="display: none;">
438
  <div class="nav-section-title">Aprendizaje</div>
439
  <a class="nav-item" data-page="ai-test"><i class="fas fa-robot"></i><span>Test de IA</span></a>
440
+ <a class="nav-item" data-page="quizzes-student"><i class="fas fa-clipboard-check"></i><span>Cuestionarios</span></a>
441
  <a class="nav-item" data-page="simulators"><i class="fas fa-microchip"></i><span>Simuladores</span></a>
442
  <a class="nav-item" data-page="raspberry"><i class="fas fa-server"></i><span>Raspberry Pi</span></a>
443
  <a class="nav-item" data-page="my-progress"><i class="fas fa-chart-line"></i><span>Mi Progreso</span></a>
 
504
  </div>
505
  </div>
506
 
507
+ <!-- STUDENT QUIZZES -->
508
+ <div class="page" id="quizzes-student">
509
+ <div class="card">
510
+ <div class="card-header"><h3 class="card-title">Mis Cuestionarios</h3></div>
511
+ <div class="card-body" id="studentQuizzesList"></div>
512
+ </div>
513
+ </div>
514
+
515
  <!-- SIMULATORS -->
516
  <div class="page" id="simulators">
517
  <div class="sim-tabs">
 
605
  </div>
606
  </div>
607
 
608
+ <!-- QUIZZES (Teacher Only) -->
609
+ <div class="page" id="quizzes">
610
+ <div class="card">
611
+ <div class="card-header">
612
+ <h3 class="card-title">Cuestionarios</h3>
613
+ <button class="btn btn-primary" onclick="QuizManager.openModal()"><i class="fas fa-plus"></i>Nuevo Cuestionario</button>
614
+ </div>
615
+ <div class="card-body" id="quizzesList"></div>
616
+ </div>
617
+ </div>
618
+
619
  <!-- STUDENT PROGRESS -->
620
  <div class="page" id="my-progress">
621
  <div class="card">
 
675
  </div>
676
  </div>
677
 
678
+ <!-- MODAL CREAR CUESTIONARIO -->
679
+ <div class="modal" id="quizModal">
680
+ <div class="modal-content" style="max-width: 700px;">
681
+ <div class="modal-header">
682
+ <h3 class="modal-title">Nuevo Cuestionario</h3>
683
+ <button class="modal-close" onclick="QuizManager.closeModal()"><i class="fas fa-times"></i></button>
684
+ </div>
685
+ <div class="modal-body">
686
+ <div class="form-group">
687
+ <label class="form-label">T铆tulo del Cuestionario</label>
688
+ <input type="text" class="form-input" id="quizTitle" placeholder="Ej: Examen parcial de IA" required>
689
+ </div>
690
+ <div class="form-group">
691
+ <label class="form-label">Descripci贸n</label>
692
+ <textarea class="form-textarea" id="quizDescription" placeholder="Instrucciones o descripci贸n..."></textarea>
693
+ </div>
694
+ <div class="form-group">
695
+ <label class="form-label">Aula</label>
696
+ <select class="form-select" id="quizClassroom" required>
697
+ <option value="">Selecciona un aula</option>
698
+ </select>
699
+ </div>
700
+ <div class="form-group">
701
+ <label class="form-label">Preguntas</label>
702
+ <div id="questionsList"></div>
703
+ <button type="button" class="btn btn-secondary" onclick="QuizManager.addQuestionField()" style="margin-top: 0.5rem;"><i class="fas fa-plus"></i> Agregar Pregunta</button>
704
+ </div>
705
+ <div class="form-actions">
706
+ <button type="button" class="btn btn-secondary" onclick="QuizManager.closeModal()">Cancelar</button>
707
+ <button type="button" class="btn btn-primary" onclick="QuizManager.createQuiz()">Crear Cuestionario</button>
708
+ </div>
709
+ </div>
710
+ </div>
711
+ </div>
712
+
713
+ <!-- MODAL RESPONDER CUESTIONARIO -->
714
+ <div class="modal" id="quizAnswerModal">
715
+ <div class="modal-content" style="max-width: 700px;">
716
+ <div class="modal-header">
717
+ <h3 class="modal-title" id="quizAnswerTitle">Cuestionario</h3>
718
+ <button class="modal-close" onclick="StudentQuizzes.closeQuizModal()"><i class="fas fa-times"></i></button>
719
+ </div>
720
+ <div class="modal-body" id="quizAnswerContent"></div>
721
+ </div>
722
+ </div>
723
+
724
  <script src="/static/js/modal.js"></script>
725
  <script src="/static/js/app.js"></script>
726
  <script src="/static/js/auth.js"></script>
 
733
  <script src="/static/js/raspberry.js"></script>
734
  <script src="/static/js/teacher.js"></script>
735
  <script src="/static/js/student_progress.js"></script>
736
+ <script src="/static/js/student_quizzes.js"></script>
737
  <script>
738
  document.addEventListener('DOMContentLoaded', () => {
739
  Auth.init();
 
748
  if (page === 'raspberry') Raspberry.init();
749
  if (page === 'progress') TeacherProgress.init();
750
  if (page === 'my-progress') StudentProgress.init();
751
+ if (page === 'quizzes') QuizManager.init();
752
+ if (page === 'quizzes-student') StudentQuizzes.init();
753
  });
754
  });
755
 
requirements.txt CHANGED
@@ -2,3 +2,5 @@ flask>=2.3.0
2
  flask-cors>=4.0.0
3
  gunicorn>=21.0.0
4
  datasets>=2.14.0
 
 
 
2
  flask-cors>=4.0.0
3
  gunicorn>=21.0.0
4
  datasets>=2.14.0
5
+ huggingface_hub>=0.19.0
6
+ requests>=2.31.0