diff --git a/LearningLens2025/.gitignore b/LearningLens2025/.gitignore index fa09c0c7..235f9de6 100644 --- a/LearningLens2025/.gitignore +++ b/LearningLens2025/.gitignore @@ -49,10 +49,12 @@ terraform/terraformout.txt # npm /lambda/code_eval/node_modules -/lambda/gettoken/node_modules +/lambda/ai_log/node_modules /lambda/game_data/node_modules -/lambda/gettoken/gettoken.zip +/lambda/reflections/node_modules +/lambda/ai_log/ai_log.zip /lambda/code_eval/code_eval.zip /lambda/game_data/game_data.zip +/lambda/reflections/reflections.zip # Any extra javascript files for lambda functions !lambda/code_eval/*.js diff --git a/LearningLens2025/frontend/.example.env b/LearningLens2025/frontend/.example.env index 5b19e89c..b1344b3e 100644 --- a/LearningLens2025/frontend/.example.env +++ b/LearningLens2025/frontend/.example.env @@ -14,4 +14,5 @@ GOOGLE_CLIENT_ID= AI_LOGGING_URL= CODE_EVAL_URL= GAME_URL= +REFLECTIONS_URL= LOCAL_MODEL_DOWNLOAD_URL_PATH= diff --git a/LearningLens2025/frontend/lib/Api/lms/moodle/moodle_lms_service.dart b/LearningLens2025/frontend/lib/Api/lms/moodle/moodle_lms_service.dart index 85df6281..42dd8d88 100644 --- a/LearningLens2025/frontend/lib/Api/lms/moodle/moodle_lms_service.dart +++ b/LearningLens2025/frontend/lib/Api/lms/moodle/moodle_lms_service.dart @@ -928,8 +928,10 @@ class MoodleLmsService implements LmsInterface { return []; } - final data = jsonDecode(response.body) as List; - if (data.isEmpty || data.first is! Map) { + final data = jsonDecode(response.body); + if (data.isEmpty || + data is! List || + data.first is! Map) { return []; } print('Rubric Grades Response: ${jsonEncode(data)}'); diff --git a/LearningLens2025/frontend/lib/Views/dashboard.dart b/LearningLens2025/frontend/lib/Views/dashboard.dart index 68dadbab..3e42d1de 100644 --- a/LearningLens2025/frontend/lib/Views/dashboard.dart +++ b/LearningLens2025/frontend/lib/Views/dashboard.dart @@ -13,6 +13,7 @@ import 'package:learninglens_app/Views/iep_page.dart'; import 'package:learninglens_app/Views/lesson_plans.dart'; import 'package:learninglens_app/Views/nav_card.dart'; import 'package:learninglens_app/Views/program_assessment_view.dart'; +import 'package:learninglens_app/Views/student_reflections_page.dart'; import 'package:learninglens_app/Views/user_settings.dart'; import 'package:learninglens_app/services/local_storage_service.dart'; @@ -361,7 +362,10 @@ class TeacherDashboard extends StatelessWidget { { 'title': 'Reflections', 'description': 'Reflect on your use of AI for your assignments.', - 'onPressed': null, + 'onPressed': () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StudentReflectionsPage())), 'icon': Icons.note_add_outlined }, ]; diff --git a/LearningLens2025/frontend/lib/Views/edit_reflection_questions_page.dart b/LearningLens2025/frontend/lib/Views/edit_reflection_questions_page.dart new file mode 100644 index 00000000..d375851d --- /dev/null +++ b/LearningLens2025/frontend/lib/Views/edit_reflection_questions_page.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; +import 'package:learninglens_app/Controller/custom_appbar.dart'; + +class EditReflectionQuestionsPage extends StatefulWidget { + final String assignmentId; + final String courseId; + final List initialQuestions; + + const EditReflectionQuestionsPage({ + Key? key, + required this.assignmentId, + required this.courseId, + required this.initialQuestions, + }) : super(key: key); + + @override + State createState() => + _EditReflectionQuestionsPageState(); +} + +class _EditReflectionQuestionsPageState + extends State { + List _controllers = []; + final _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _controllers = widget.initialQuestions + .map((q) => TextEditingController(text: q)) + .toList(); + if (_controllers.isEmpty) { + _controllers.add(TextEditingController()); + } + } + + void addQuestionField() { + setState(() { + _controllers.add(TextEditingController()); + }); + } + + void removeQuestionField(int index) { + setState(() { + _controllers.removeAt(index); + }); + } + + Future saveQuestions() async { + if (!_formKey.currentState!.validate()) return; + + List questions = + _controllers.map((controller) => controller.text.trim()).toList(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Questions saved successfully!')), + ); + Navigator.pop(context, questions); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + title: 'Edit Reflection Questions', + userprofileurl: LmsFactory.getLmsService().profileImage ?? '', + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Reflection Questions', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + SizedBox(height: 12), + ..._controllers.asMap().entries.map( + (entry) { + int index = entry.key; + TextEditingController controller = entry.value; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: Row( + children: [ + Expanded( + child: TextFormField( + controller: controller, + decoration: InputDecoration( + labelText: 'Question ${index + 1}', + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter a question'; + } + return null; + }, + ), + ), + SizedBox(width: 8), + if (_controllers.length > 1) + IconButton( + icon: Icon(Icons.delete, color: Colors.red), + onPressed: () => removeQuestionField(index), + ), + ], + ), + ); + }, + ), + SizedBox(height: 12), + Row( + children: [ + ElevatedButton.icon( + onPressed: addQuestionField, + icon: Icon(Icons.add), + label: Text('Add Question'), + ), + SizedBox(width: 12), + ElevatedButton( + onPressed: saveQuestions, + child: Text('Save Questions'), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/LearningLens2025/frontend/lib/Views/gamification_view.dart b/LearningLens2025/frontend/lib/Views/gamification_view.dart index 0f99e619..2b4dc803 100644 --- a/LearningLens2025/frontend/lib/Views/gamification_view.dart +++ b/LearningLens2025/frontend/lib/Views/gamification_view.dart @@ -110,8 +110,8 @@ class _GamificationViewState extends State { }); } else { games = await _gamificationService.getGamesForStudent(userId); - final completed = games.where((g) => g.score != null).toList(); - final pending = games.where((g) => g.score == null).toList(); + final completed = games.where((g) => g.score!.score != null).toList(); + final pending = games.where((g) => g.score!.score == null).toList(); await _ensureCourseNames(); setState(() { assignedGames = pending; @@ -906,19 +906,22 @@ $text }); try { + final assignedGame = AssignedGame( + uuid: null, + courseId: courseId, + gameType: gameTypeEnum, + title: title, + gameData: contentPayload, + assignedDate: now, + assignedBy: teacherId, + ); + final gameResponse = await _gamificationService.createGame(assignedGame); + final responseBody = jsonDecode(gameResponse.body); + final gameId = responseBody[0]["game_id"]; + await Future.wait(targetStudents.map((student) { - final assignedGame = AssignedGame( - uuid: null, - studentId: student.id, - courseId: courseId, - gameType: gameTypeEnum, - title: title, - gameData: contentPayload, - assignedDate: now, - assignedBy: teacherId, - studentName: '${student.firstname} ${student.lastname}'.trim(), - ); - return _gamificationService.createGame(assignedGame); + return _gamificationService + .assignGame(AssignedGameScore(studentId: student.id, game: gameId)); })); await _refreshAssignments(); @@ -1303,6 +1306,7 @@ $text try { final response = await _gamificationService.completeGame( game.uuid!, + game.score!.studentId, normalizedScore, rawCorrect: result.score, maxScore: result.maxScore, @@ -1447,7 +1451,7 @@ $text final groupedByStudent = >{}; for (final game in games) { groupedByStudent - .putIfAbsent(game.studentId, () => []) + .putIfAbsent(game.score!.studentId, () => []) .add(game); } @@ -1459,13 +1463,13 @@ $text final studentGames = entry.value ..sort((a, b) => b.assignedDate.compareTo(a.assignedDate)); for (final game in studentGames) { - final hasRaw = game.rawCorrect != null && - game.maxScore != null && - game.maxScore! > 0; - final isCompleted = game.score != null; + final hasRaw = game.score!.rawCorrect != null && + game.score!.maxScore != null && + game.score!.maxScore! > 0; + final isCompleted = game.score!.score != null; final statusText = isCompleted ? hasRaw - ? 'Completed ${game.rawCorrect}/${game.maxScore}' + ? 'Completed ${game.score!.rawCorrect}/${game.score!.maxScore}' : 'Completed' : 'Pending'; rows.add( diff --git a/LearningLens2025/frontend/lib/Views/send_essay_to_moodle.dart b/LearningLens2025/frontend/lib/Views/send_essay_to_moodle.dart index 0e4567cd..99ac2af1 100644 --- a/LearningLens2025/frontend/lib/Views/send_essay_to_moodle.dart +++ b/LearningLens2025/frontend/lib/Views/send_essay_to_moodle.dart @@ -3,8 +3,11 @@ import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; import 'package:learninglens_app/Controller/custom_appbar.dart'; import 'package:learninglens_app/beans/course.dart'; import 'package:learninglens_app/Views/dashboard.dart'; +import 'package:learninglens_app/Views/edit_reflection_questions_page.dart'; import 'dart:convert'; +import 'package:learninglens_app/services/reflection_service.dart'; + class EssayAssignmentSettings extends StatefulWidget { final String updatedJson; final String description; @@ -18,6 +21,7 @@ class EssayAssignmentSettings extends StatefulWidget { class EssayAssignmentSettingsState extends State { // Global key for the form final _formKey = GlobalKey(); + List reflectionQuestions = []; // Date selection variables for "Allow submissions from" String selectedDaySubmission = '01'; @@ -232,8 +236,7 @@ class EssayAssignmentSettingsState extends State { double screenWidth = constraints.maxWidth; // Example: Calculate sizes dynamically based on screen width - double buttonWidth = - screenWidth * 0.4; // Buttons take 40% of screen width + double buttonWidth = 275.0; double descriptionHeight = screenWidth * 0.2; // Description box takes 20% of screen width height @@ -471,7 +474,7 @@ class EssayAssignmentSettingsState extends State { String allowSubmissionFrom = '$selectedDaySubmission $selectedMonthSubmission $selectedYearSubmission $selectedHourSubmission:$selectedMinuteSubmission'; - await api.createAssignment( + final result = await api.createAssignment( courseId, sectionNumber, // Section ID assignmentName, @@ -481,6 +484,16 @@ class EssayAssignmentSettingsState extends State { description, ); + print(result); + + for (String r in reflectionQuestions) { + await ReflectionService().createReflection( + Reflection( + courseId: int.parse(courseId), + assignmentId: result?['assignmentid'], + question: r)); + } + if (mounted) { final snackBar = SnackBar( content: Text( @@ -526,6 +539,53 @@ class EssayAssignmentSettingsState extends State { child: Text('Go Back to Edit Essay'), ), ), + SizedBox( + width: buttonWidth, + child: ElevatedButton.icon( + onPressed: () async { + // Get course ID from selected course + Course? selectedCourseObj = courses.firstWhere( + (c) => c.fullName == selectedCourse, + orElse: () => Course(0, '', '', '', + DateTime.now(), DateTime.now())); + if (selectedCourseObj.id == 0) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Please select a course first.')), + ); + return; + } + + String assignmentId = + _assignmentNameController.text.isNotEmpty + ? _assignmentNameController.text + : 'temp_assignment'; + + final updatedQuestions = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + EditReflectionQuestionsPage( + courseId: selectedCourseObj.id.toString(), + assignmentId: assignmentId, + initialQuestions: [], + ), + ), + ); + + if (updatedQuestions != null && + updatedQuestions is List) { + // Store the questions locally in state + setState(() { + reflectionQuestions = updatedQuestions; + }); + } + }, + icon: Icon(Icons.edit), + label: Text('Edit Reflection Questions'), + ), + ), ], ), ], diff --git a/LearningLens2025/frontend/lib/Views/student_reflections_page.dart b/LearningLens2025/frontend/lib/Views/student_reflections_page.dart new file mode 100644 index 00000000..ed1bf962 --- /dev/null +++ b/LearningLens2025/frontend/lib/Views/student_reflections_page.dart @@ -0,0 +1,339 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:learninglens_app/services/reflection_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +// Api +import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; + +// beans +import 'package:learninglens_app/beans/assignment.dart'; +import 'package:learninglens_app/beans/course.dart'; + +enum ReflectionStatus { notStarted, inProgress, submitted } + +class StudentReflectionsPage extends StatefulWidget { + @override + State createState() => _StudentReflectionsPageState(); +} + +class _StudentReflectionsPageState extends State { + List _essays = []; + int _selectedSidebarIndex = -1; + bool _isReloadingEssays = false; + + // Keep track of each essay’s reflection status + final Map _essayStatus = {}; + + // Track reflection answers for each essay + final Map> _controllers = {}; + + @override + void initState() { + super.initState(); + _loadEssays(); + } + + Future _loadEssays({int? courseId}) async { + setState(() => _isReloadingEssays = true); + + try { + final allEssays = await getAllEssays(courseId); + + final prefs = await SharedPreferences.getInstance(); + final uid = prefs.getString('userId'); + final int? uidInt = uid != null ? int.tryParse(uid) : null; + + final filteredEssays = allEssays.where((a) => !_isOverdue(a)).toList() + ..sort((a, b) { + final ad = _effectiveDue(a); + final bd = _effectiveDue(b); + if (ad == null && bd == null) return 0; + if (ad == null) return 1; + if (bd == null) return -1; + return ad.compareTo(bd); + }); + + for (var e in filteredEssays) { + ReflectionStatus stat = ReflectionStatus.notStarted; + final refs = await ReflectionService() + .getReflectionsForAssignment(e.courseId, e.id); + _controllers[e.id.toString()] = {}; + for (var r in refs) { + ReflectionResponse? response; + if (uidInt != null) { + response = await ReflectionService() + .getReflectionForSubmission(r.uuid!, uidInt); + if (response != null) { + stat = ReflectionStatus.submitted; + } + } + setState(() { + _controllers[e.id.toString()]![r] = + TextEditingController(text: response?.response ?? ""); + }); + } + setState(() { + _essayStatus[e.id.toString()] = stat; + }); + } + + setState(() { + _essays = filteredEssays; + }); + + setState(() => _isReloadingEssays = false); + } catch (e) { + print("Error loading essays: $e"); + } + } + + bool _isOverdue(Assignment a) => + a.dueDate != null && a.dueDate!.isBefore(DateTime.now()); + DateTime? _effectiveDue(Assignment a) => a.dueDate; + + String _formatDate(DateTime? date) { + if (date == null) return 'No due date'; + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + } + + void _startReflection(Assignment essay) async { + final id = essay.id.toString(); + setState(() { + _essayStatus[id] = ReflectionStatus.inProgress; + }); + } + + void _submitReflection(Assignment essay) async { + final id = essay.id.toString(); + final controllers = _controllers[id]; + final prefs = await SharedPreferences.getInstance(); + final uid = prefs.getString('userId'); + final int? uidInt = uid != null ? int.tryParse(uid) : null; + if (controllers == null || uidInt == null) return; + + for (var c in controllers.entries) { + await ReflectionService().completeReflection(ReflectionResponse( + studentId: uidInt, + response: c.value.text.trim(), + reflectionId: c.key.uuid!)); + } + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Reflection submitted.')), + ); + + setState(() { + _essayStatus[id] = ReflectionStatus.submitted; + }); + } + + Widget _statusChip(Assignment a) { + final status = _essayStatus[a.id.toString()] ?? ReflectionStatus.notStarted; + switch (status) { + case ReflectionStatus.notStarted: + return const Chip(label: Text('Not Started')); + case ReflectionStatus.inProgress: + return Chip( + label: const Text('In Progress'), + backgroundColor: Colors.amber.withOpacity(.2)); + case ReflectionStatus.submitted: + return Chip( + label: const Text('Submitted'), + backgroundColor: Colors.green.withOpacity(.2)); + } + } + + @override + Widget build(BuildContext context) { + final selectedEssay = + _selectedSidebarIndex >= 0 ? _essays[_selectedSidebarIndex] : null; + + return Scaffold( + appBar: AppBar(title: const Text('Student Reflections')), + body: Row( + children: [ + // Left Sidebar: Essay List + SizedBox( + width: 280, + child: Material( + color: Theme.of(context).colorScheme.surface, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), + child: Row( + children: [ + const Icon(Icons.assignment_outlined, size: 20), + const SizedBox(width: 8), + Text('Essay Assignments', + style: Theme.of(context).textTheme.titleMedium), + const Spacer(), + ], + ), + ), + const Divider(height: 1), + _isReloadingEssays + ? CircularProgressIndicator() + : Expanded( + child: ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(8), + itemCount: _essays.length, + separatorBuilder: (_, __) => + const Divider(height: 1), + itemBuilder: (context, i) { + final assignment = _essays[i]; + final selected = _selectedSidebarIndex == i; + final dueText = _formatDate(assignment.dueDate); + + return ListTile( + dense: true, + selected: selected, + selectedTileColor: Theme.of(context) + .colorScheme + .primary + .withOpacity(0.08), + title: Text(assignment.name, + maxLines: 1, + overflow: TextOverflow.ellipsis), + subtitle: Text('Due: $dueText'), + trailing: _statusChip(assignment), + onTap: () => + setState(() => _selectedSidebarIndex = i), + ); + }, + ), + ), + ], + ), + ), + ), + + const VerticalDivider(width: 1), + + // Right Pane: Essay Details + Reflection + Expanded( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: selectedEssay == null + ? const Center( + child: Text( + "Select an assignment to begin reflection.", + style: TextStyle(color: Colors.grey), + ), + ) + : SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Assignment: ${selectedEssay.name}', + style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 10), + Text('Course: ${selectedEssay.courseId}', + style: Theme.of(context).textTheme.bodyLarge), + const SizedBox(height: 10), + Text('Due: ${_formatDate(selectedEssay.dueDate)}', + style: Theme.of(context).textTheme.bodyLarge), + const SizedBox(height: 10), + const Text('Description:', + style: TextStyle(fontWeight: FontWeight.bold)), + Html( + data: selectedEssay.description, + style: { + "body": Style( + margin: Margins.zero, + padding: HtmlPaddings.zero, + ), + }, + ), + const SizedBox(height: 20), + _buildReflectionSection(selectedEssay), + ], + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildReflectionSection(Assignment essay) { + final id = essay.id.toString(); + final status = _essayStatus[id] ?? ReflectionStatus.notStarted; + + if (status == ReflectionStatus.notStarted) { + return ElevatedButton( + onPressed: () => _startReflection(essay), + child: const Text('Start Reflection'), + ); + } else if (status == ReflectionStatus.inProgress) { + final controllers = _controllers[id]!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Reflection Questions:', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + const SizedBox(height: 12), + ...controllers.keys.map((q) { + return Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(q.question, + style: const TextStyle( + fontWeight: FontWeight.w600, fontSize: 16)), + const SizedBox(height: 6), + TextField( + controller: controllers[q], + maxLines: 3, + decoration: InputDecoration( + hintText: 'Enter your response...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ], + ), + ); + }), + const SizedBox(height: 16), + Center( + child: ElevatedButton( + onPressed: () => _submitReflection(essay), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + padding: + const EdgeInsets.symmetric(horizontal: 40, vertical: 14), + ), + child: const Text( + 'Submit Reflection', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ), + ), + ], + ); + } else { + return const Padding( + padding: EdgeInsets.only(top: 12), + child: Text('Reflection submitted!', + style: TextStyle(color: Colors.green, fontWeight: FontWeight.bold)), + ); + } + } +} + +// Helper to fetch all essays from LMS +Future> getAllEssays(int? courseID) async { + List result = []; + for (Course c in LmsFactory.getLmsService().courses ?? []) { + if (courseID == 0 || courseID == null || c.id == courseID) { + result.addAll(c.essays ?? []); + } + } + return result; +} diff --git a/LearningLens2025/frontend/lib/Views/view_reflection_page.dart b/LearningLens2025/frontend/lib/Views/view_reflection_page.dart index ca200d82..4bca91b3 100644 --- a/LearningLens2025/frontend/lib/Views/view_reflection_page.dart +++ b/LearningLens2025/frontend/lib/Views/view_reflection_page.dart @@ -5,29 +5,17 @@ import 'package:learninglens_app/beans/submission.dart'; class ViewReflectionPage extends StatelessWidget { final Participant participant; final Submission submission; + final List> reflections; - const ViewReflectionPage({ - super.key, - required this.participant, - required this.submission, - }); + const ViewReflectionPage( + {super.key, + required this.participant, + required this.submission, + required this.reflections}); @override Widget build(BuildContext context) { // Example reflection data - final Map reflectionData = { - 'How did you approach this task before using AI support?': - 'I started by outlining the main points I wanted to cover before consulting AI tools.', - 'In what ways did AI assistance influence your thought process or decisions?': - 'AI helped me rephrase my arguments more clearly and provided feedback on structure.', - 'What challenges did you face while completing this task?': - 'It was difficult balancing my own ideas with AI suggestions without losing authenticity.', - 'How confident are you in your final submission and why?': - 'Fairly confident. I double-checked all content and ensured originality.', - 'What would you do differently next time to improve your work?': - 'Spend more time planning before using AI to ensure I stay in control of my ideas.' - }; - return Scaffold( appBar: AppBar( title: Text('Reflection for ${participant.fullname}'), @@ -35,14 +23,14 @@ class ViewReflectionPage extends StatelessWidget { body: Padding( padding: const EdgeInsets.all(16.0), child: ListView( - children: reflectionData.entries.map((entry) { + children: reflections.map((entry) { return Padding( padding: const EdgeInsets.only(bottom: 20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - entry.key, + entry[0], style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -51,7 +39,7 @@ class ViewReflectionPage extends StatelessWidget { ), const SizedBox(height: 8), TextField( - controller: TextEditingController(text: entry.value), + controller: TextEditingController(text: entry[1]), readOnly: true, minLines: 3, maxLines: 6, diff --git a/LearningLens2025/frontend/lib/Views/view_submission_detail.dart b/LearningLens2025/frontend/lib/Views/view_submission_detail.dart index 2176bb32..2ca8b82b 100644 --- a/LearningLens2025/frontend/lib/Views/view_submission_detail.dart +++ b/LearningLens2025/frontend/lib/Views/view_submission_detail.dart @@ -9,6 +9,7 @@ import 'dart:math'; import 'package:learninglens_app/Views/view_reflection_page.dart'; import 'package:learninglens_app/beans/submission_with_grade.dart'; +import 'package:learninglens_app/services/reflection_service.dart'; class SubmissionDetail extends StatefulWidget { final Participant participant; @@ -35,6 +36,7 @@ class SubmissionDetailState extends State { {}; // Controllers for each remark double? calculatedGrade; + // List to store reflections @override void initState() { super.initState(); @@ -112,6 +114,18 @@ class SubmissionDetailState extends State { return (totalAchieved / totalPossible) * 100; } + Future>> fetchReflections() async { + List> reflectionsToReturn = []; + final reflections = await ReflectionService().getReflectionsForAssignment( + int.parse(widget.courseId), widget.submission.submission.assignmentId); + for (Reflection r in reflections) { + final resp = await ReflectionService() + .getReflectionForSubmission(r.uuid!, widget.participant.id); + reflectionsToReturn.add([r.question, resp?.response ?? ""]); + } + return reflectionsToReturn; + } + // Save updated submission scores and remarks as JSON void saveSubmissionScores() async { List> updatedScores = []; @@ -243,17 +257,21 @@ class SubmissionDetailState extends State { fontSize: 18, fontWeight: FontWeight.bold), ), ElevatedButton.icon( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ViewReflectionPage( - participant: widget.participant, - submission: - widget.submission.submission, - ), - ), - ); + onPressed: () async { + fetchReflections() + .then((value) => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ViewReflectionPage( + participant: + widget.participant, + submission: widget + .submission + .submission, + reflections: value), + ), + )); }, icon: Icon(Icons.note_alt_outlined), label: Text('View Reflection'), diff --git a/LearningLens2025/frontend/lib/main.dart b/LearningLens2025/frontend/lib/main.dart index 9ae4d3f2..998c4965 100644 --- a/LearningLens2025/frontend/lib/main.dart +++ b/LearningLens2025/frontend/lib/main.dart @@ -8,8 +8,10 @@ import 'package:learninglens_app/Views/program_assessment_view.dart'; import 'package:learninglens_app/Views/user_settings.dart'; import 'package:learninglens_app/notifiers/login_notifier.dart'; import 'package:learninglens_app/notifiers/theme_notifier.dart'; +import 'package:learninglens_app/services/gamification_service.dart'; import 'package:learninglens_app/services/local_storage_service.dart'; import 'package:learninglens_app/services/program_assessment_service.dart'; +import 'package:learninglens_app/services/reflection_service.dart'; import 'package:provider/provider.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -30,6 +32,8 @@ void main() async { await AILoggingSingleton().createDb(); await AILoggingSingleton().clearOldDatabaseEntries(); await ProgramAssessmentService.createDb(); + await GamificationService.createDb(); + await ReflectionService.createDb(); runApp( MultiProvider( diff --git a/LearningLens2025/frontend/lib/services/gamification_service.dart b/LearningLens2025/frontend/lib/services/gamification_service.dart index c652be04..12416409 100644 --- a/LearningLens2025/frontend/lib/services/gamification_service.dart +++ b/LearningLens2025/frontend/lib/services/gamification_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:developer' as developer; import 'package:http/http.dart' as http; +import 'package:learninglens_app/Api/lms/enum/lms_enum.dart'; import 'package:learninglens_app/services/api_service.dart'; import 'package:learninglens_app/services/local_storage_service.dart'; @@ -12,26 +13,38 @@ enum GameType { FLASHCARD, MATCHING, QUIZ } // AssignedGame model class AssignedGame { final String? uuid; - final int studentId; final int courseId; final GameType gameType; final String title; final String gameData; final int assignedBy; final DateTime assignedDate; - final String? studentName; - final int? rawCorrect; - final int? maxScore; - double? score; + LmsType lms = LocalStorageService.getSelectedClassroom(); + AssignedGameScore? score; AssignedGame( - {required this.studentId, - required this.courseId, + {required this.courseId, required this.gameType, required this.title, required this.gameData, required this.assignedDate, required this.assignedBy, + this.uuid, + this.score}); +} + +class AssignedGameScore { + final String? uuid; + final int studentId; + final String? studentName; + final int? rawCorrect; + final int? maxScore; + double? score; + final String game; + + AssignedGameScore( + {required this.studentId, + required this.game, this.studentName, this.rawCorrect, this.maxScore, @@ -109,18 +122,25 @@ class GamificationService { return await ApiService().httpPost(uri, body: jsonEncode({ 'courseId': game.courseId, - 'studentId': game.studentId, 'gameType': game.gameType.index, 'title': game.title, 'data': game.gameData, 'assignedBy': game.assignedBy, 'assignedDate': game.assignedDate.toString(), + 'lmsType': game.lms.index, })); } + Future assignGame(AssignedGameScore score) async { + final uri = _requireUri('assignGame'); + return await ApiService().httpPost(uri, + body: jsonEncode({'studentId': score.studentId, 'game': score.game})); + } + /// Starts a program assessment Future completeGame( String uuid, + int studentId, double score, { int? rawCorrect, int? maxScore, @@ -129,6 +149,7 @@ class GamificationService { return await ApiService().httpPost(uri, body: jsonEncode({ 'gameId': uuid, + 'studentId': studentId, 'score': score, 'rawCorrect': rawCorrect, 'maxScore': maxScore, @@ -139,7 +160,10 @@ class GamificationService { Future> getGamesForTeacher(int createdBy) async { final uri = _requireUri( 'getForTeacher', - params: {'createdBy': '$createdBy'}, + params: { + 'createdBy': '$createdBy', + 'lmsType': '${LocalStorageService.getSelectedClassroom().index}' + }, ); final response = await ApiService().httpGet(uri); @@ -152,7 +176,10 @@ class GamificationService { Future> getGamesForStudent(int assignedTo) async { final uri = _requireUri( 'getForStudent', - params: {'assignedTo': '$assignedTo'}, + params: { + 'assignedTo': '$assignedTo', + 'lmsType': '${LocalStorageService.getSelectedClassroom().index}' + }, ); final response = await ApiService().httpGet(uri); @@ -186,23 +213,24 @@ class GamificationService { } return AssignedGame( - uuid: eval['game_id'], - courseId: int.parse(eval['course_id']), - studentId: int.parse(eval['student_id']), - gameType: type, - title: eval['title'], - gameData: gameData, - assignedBy: int.parse(eval['assigned_by']), - assignedDate: DateTime.parse(eval['assigned_date']), - studentName: eval['student_name'], - rawCorrect: eval['raw_correct'] == null - ? null - : int.tryParse(eval['raw_correct'].toString()), - maxScore: eval['max_score'] == null - ? null - : int.tryParse(eval['max_score'].toString()), - score: parsedScore, - ); + uuid: eval['game_id'], + courseId: int.parse(eval['course_id']), + gameType: type, + title: eval['title'], + gameData: gameData, + assignedBy: int.parse(eval['assigned_by']), + assignedDate: DateTime.parse(eval['assigned_date']), + score: AssignedGameScore( + studentId: int.parse(eval['student_id']), + studentName: eval['student_name'], + rawCorrect: eval['raw_correct'] == null + ? null + : int.tryParse(eval['raw_correct'].toString()), + game: eval['game_id'], + maxScore: eval['max_score'] == null + ? null + : int.tryParse(eval['max_score'].toString()), + score: parsedScore)); }).toList(); } diff --git a/LearningLens2025/frontend/lib/services/local_storage_service.dart b/LearningLens2025/frontend/lib/services/local_storage_service.dart index 8ea8935b..9232e816 100644 --- a/LearningLens2025/frontend/lib/services/local_storage_service.dart +++ b/LearningLens2025/frontend/lib/services/local_storage_service.dart @@ -348,6 +348,16 @@ class LocalStorageService { return url; } + static String getReflectionsUrl() { + String url = _prefs.getString('REFLECTIONS_URL') ?? + dotenv.env['REFLECTIONS_URL'] ?? + ''; + if (url.endsWith('/')) { + url = url.substring(0, url.length - 1); + } + return url; + } + static void clearAILoggingUrl() { _prefs.remove('AI_LOGGING_URL'); } @@ -360,6 +370,10 @@ class LocalStorageService { _prefs.remove('GAME_URL'); } + static void clearReflectionsUrl() { + _prefs.remove('REFLECTIONS_URL'); + } + static hasLLMKey() { return getOpenAIKey().isNotEmpty || getGrokKey().isNotEmpty || diff --git a/LearningLens2025/frontend/lib/services/reflection_service.dart b/LearningLens2025/frontend/lib/services/reflection_service.dart new file mode 100644 index 00000000..8e66bcb1 --- /dev/null +++ b/LearningLens2025/frontend/lib/services/reflection_service.dart @@ -0,0 +1,183 @@ +import 'dart:convert'; +import 'dart:developer' as developer; + +import 'package:http/http.dart' as http; +import 'package:learninglens_app/Api/lms/enum/lms_enum.dart'; +import 'package:learninglens_app/services/api_service.dart'; +import 'package:learninglens_app/services/local_storage_service.dart'; + +/// Represents a program assessment job +/// Check the handleGET method in code_eval/index.mjs for properties +// AssignedGame model +class Reflection { + final String? uuid; + final int courseId; + final int assignmentId; + final String question; + final DateTime date = DateTime.now(); + LmsType lms = LocalStorageService.getSelectedClassroom(); + + Reflection( + {required this.courseId, + required this.assignmentId, + required this.question, + this.uuid}); +} + +class ReflectionResponse { + final String? uuid; + final String reflectionId; + final int studentId; + final String response; + final DateTime date = DateTime.now(); + + ReflectionResponse( + {required this.studentId, + required this.response, + required this.reflectionId, + this.uuid}); +} + +class ReflectionService { + final reflectionUrl = LocalStorageService.getReflectionsUrl(); + + static Uri? _buildCommandUri( + String baseUrl, + String command, { + Map? params, + }) { + final trimmed = baseUrl.trim(); + if (trimmed.isEmpty) { + return null; + } + + final parsed = Uri.tryParse(trimmed); + if (parsed == null || parsed.scheme.isEmpty) { + return null; + } + + final query = {'command': command}; + if (params != null) { + query.addAll(params); + } + + final path = parsed.path.isEmpty ? '/' : parsed.path; + return parsed.replace(path: path, queryParameters: query); + } + + Uri _requireUri( + String command, { + Map? params, + }) { + final uri = _buildCommandUri(reflectionUrl, command, params: params); + if (uri == null) { + throw StateError( + 'Reflection service URL not configured. Please verify the REFLECTION_URL setting.', + ); + } + return uri; + } + + static Future createDb() async { + final baseUrl = LocalStorageService.getReflectionsUrl(); + final uri = _buildCommandUri(baseUrl, 'createDb'); + + if (uri == null) { + developer.log( + 'Skipping reflection database init; REFLECTION_URL is not set or invalid.', + name: 'ReflectionService', + ); + return; + } + + try { + await http.post(uri); + } catch (error, stackTrace) { + developer.log( + 'Failed to initialize reflection database.', + name: 'ReflectionService', + error: error, + stackTrace: stackTrace, + ); + } + } + + /// Starts a program assessment + Future createReflection(Reflection ref) async { + final uri = _requireUri('createReflection'); + return await ApiService().httpPost(uri, + body: jsonEncode({ + 'courseId': ref.courseId, + 'assignmentId': ref.assignmentId, + 'question': ref.question, + 'lmsType': ref.lms.index, + })); + } + + /// Starts a program assessment + Future completeReflection(ReflectionResponse resp) async { + final uri = _requireUri('completeReflection'); + return await ApiService().httpPost(uri, + body: jsonEncode({ + 'studentId': resp.studentId, + 'response': resp.response, + 'reflectionId': resp.reflectionId + })); + } + + /// Gets code evaluations for all assignments in a course + Future> getReflectionsForAssignment( + int courseId, int assignmentId) async { + final uri = _requireUri( + 'getReflection', + params: { + 'courseId': '$courseId', + 'assignmentId': '$assignmentId', + 'lmsType': '${LocalStorageService.getSelectedClassroom().index}' + }, + ); + final response = await ApiService().httpGet(uri); + + if (response.statusCode != 200) return []; + + final reflections = jsonDecode(response.body) as List; + return reflections.map((ref) { + return Reflection( + uuid: ref['reflection_id'], + courseId: courseId, + assignmentId: assignmentId, + question: ref['question'], + ); + }).toList(); + } + + /// Gets code evaluations for all assignments in a course + Future getReflectionForSubmission( + String reflectionId, int studentId) async { + final uri = _requireUri( + 'getCompletedReflection', + params: {'reflectionId': reflectionId, 'studentId': '$studentId'}, + ); + final response = await ApiService().httpGet(uri); + + if (response.statusCode != 200) return null; + + final reflections = jsonDecode(response.body); + + if (reflections is List) { + return reflections + .map((ref) { + return ReflectionResponse( + uuid: ref['response_id'], + studentId: studentId, + response: ref['response'], + reflectionId: ref['reflection'], + ); + }) + .toList() + .firstOrNull; + } else { + return null; + } + } +} diff --git a/LearningLens2025/frontend/pubspec.yaml b/LearningLens2025/frontend/pubspec.yaml index f27cabba..7d2cf646 100644 --- a/LearningLens2025/frontend/pubspec.yaml +++ b/LearningLens2025/frontend/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: sdk: flutter quill_delta: ^3.0.0-nullsafety.2 vsc_quill_delta_to_html: ^1.0.5 + flutter_html: ^3.0.0 super_clipboard: ^0.9.1 flutter_quill_delta_from_html: ^1.5.3 markdown: ^7.3.0 diff --git a/LearningLens2025/lambda/gettoken/index.mjs b/LearningLens2025/lambda/ai_log/index.mjs similarity index 100% rename from LearningLens2025/lambda/gettoken/index.mjs rename to LearningLens2025/lambda/ai_log/index.mjs diff --git a/LearningLens2025/lambda/gettoken/package-lock.json b/LearningLens2025/lambda/ai_log/package-lock.json similarity index 96% rename from LearningLens2025/lambda/gettoken/package-lock.json rename to LearningLens2025/lambda/ai_log/package-lock.json index c1f8ffb1..b3d2e03e 100644 --- a/LearningLens2025/lambda/gettoken/package-lock.json +++ b/LearningLens2025/lambda/ai_log/package-lock.json @@ -1,5 +1,5 @@ { - "name": "gettoken", + "name": "ai_log", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/LearningLens2025/lambda/gettoken/package.json b/LearningLens2025/lambda/ai_log/package.json similarity index 100% rename from LearningLens2025/lambda/gettoken/package.json rename to LearningLens2025/lambda/ai_log/package.json diff --git a/LearningLens2025/lambda/game_data/gettoken.zip b/LearningLens2025/lambda/game_data/gettoken.zip deleted file mode 100644 index 77cf4b05..00000000 Binary files a/LearningLens2025/lambda/game_data/gettoken.zip and /dev/null differ diff --git a/LearningLens2025/lambda/game_data/index.mjs b/LearningLens2025/lambda/game_data/index.mjs index 519c6ae9..0cee1162 100644 --- a/LearningLens2025/lambda/game_data/index.mjs +++ b/LearningLens2025/lambda/game_data/index.mjs @@ -34,11 +34,13 @@ export const handler = async (event, context) => { if (method === "GET") { if (command === "getForStudent") { const assignedTo = BigInt(event["queryStringParameters"]["assignedTo"]); - return await getGamesForStudent(client, assignedTo); + const lms = parseInt(event["queryStringParameters"]["lmsType"]); + return await getGamesForStudent(client, assignedTo, lms); } if (command === "getForTeacher") { const createdBy = BigInt(event["queryStringParameters"]["createdBy"]); - return await getGamesForTeacher(client, createdBy); + const lms = parseInt(event["queryStringParameters"]["lmsType"]); + return await getGamesForTeacher(client, createdBy, lms); } } if (method === "POST") { @@ -51,8 +53,11 @@ export const handler = async (event, context) => { return "Completed game successfully."; } if (command === "createGame") { - await createGame(client, event["body"]); - return "Added game successfully"; + return createGame(client, event["body"]); + } + if (command === "assignGame") { + await assignGame(client, event["body"]); + return "Assigned game successfully"; } } if (method === "DELETE") { @@ -66,23 +71,21 @@ export const handler = async (event, context) => { await client`CREATE TABLE IF NOT EXISTS GAMES ( game_id UUID PRIMARY KEY, course_id BIGINT, - student_id BIGINT, title VARCHAR, data VARCHAR, game_type SMALLINT, + assigned_by BIGINT, + assigned_date TIMESTAMP, + lms_service SMALLINT + );`; + await client`CREATE TABLE IF NOT EXISTS GAME_SCORES ( + score_id UUID PRIMARY KEY, + student_id BIGINT, score NUMERIC(5,2), raw_correct SMALLINT, max_score SMALLINT, - assigned_by BIGINT, - assigned_date TIMESTAMP + game UUID );`; - try { - await client`ALTER TABLE GAMES ALTER COLUMN score TYPE NUMERIC(5,2);`; - await client`ALTER TABLE GAMES ADD COLUMN IF NOT EXISTS raw_correct SMALLINT;`; - await client`ALTER TABLE GAMES ADD COLUMN IF NOT EXISTS max_score SMALLINT;`; - } catch (alterError) { - console.warn("Skipping score column alter: ", alterError); - } } catch (error) { console.error("Failed to create database table: ", error); @@ -93,6 +96,7 @@ export const handler = async (event, context) => { async function deleteGame(client, body) { try { let log = JSON.parse(body); + await client`DELETE FROM GAME_SCORES WHERE game = ${log.gameId};`; await client`DELETE FROM GAMES WHERE game_id = ${log.gameId};`; } catch (error) { @@ -101,10 +105,11 @@ export const handler = async (event, context) => { } }; - async function getGamesForStudent(client, studentId) { + async function getGamesForStudent(client, studentId, lms) { try { - return await client`SELECT game_id, course_id, student_id, title, data, game_type, score, raw_correct, max_score, assigned_by, assigned_date FROM GAMES WHERE - student_id = ${studentId};`; + return await client`SELECT game_id, course_id, student_id, title, data, game_type, score, raw_correct, max_score, assigned_by, lms_service, assigned_date FROM GAMES INNER JOIN GAME_SCORES ON game = game_id WHERE + student_id = ${studentId} + AND lms_service = ${lms};`; } catch (error) { console.error("Failed to get games for student ", error); @@ -112,10 +117,11 @@ export const handler = async (event, context) => { } }; - async function getGamesForTeacher(client, assignedBy) { + async function getGamesForTeacher(client, assignedBy, lms) { try { - return await client`SELECT game_id, course_id, student_id, title, data, game_type, score, raw_correct, max_score, assigned_by, assigned_date FROM GAMES WHERE - assigned_by = ${assignedBy};`; + return await client`SELECT game_id, course_id, student_id, title, data, game_type, score, raw_correct, max_score, assigned_by, assigned_date FROM GAMES INNER JOIN GAME_SCORES ON game = game_id WHERE + assigned_by = ${assignedBy} + AND lms_service = ${lms};`; } catch (error) { console.error("Failed to get games for teacher ", error); @@ -123,43 +129,63 @@ export const handler = async (event, context) => { } }; - async function createGame(client, body) { + async function createGame(client, body) { try { let log = JSON.parse(body); return await client` INSERT INTO GAMES ( game_id, course_id, - student_id, title, data, game_type, - score, - raw_correct, - max_score, assigned_by, - assigned_date + assigned_date, + lms_service ) VALUES ( gen_random_uuid(), ${log.courseId}, - ${log.studentId}, ${log.title}, ${log.data}, ${log.gameType}, + ${log.assignedBy}, + ${log.assignedDate}, + ${log.lmsType} + ) RETURNING game_id;`; + } + catch (error) { + console.error("Failed to add game ", error); + throw error; + } + }; + + async function assignGame(client, body) { + try { + let log = JSON.parse(body); + return await client` + INSERT INTO GAME_SCORES ( + score_id, + student_id, + score, + raw_correct, + max_score, + game + ) VALUES ( + gen_random_uuid(), + ${log.studentId}, NULL, NULL, NULL, - ${log.assignedBy}, - ${log.assignedDate} + ${log.game} );`; } catch (error) { - console.error("Failed to add game ", error); + console.error("Failed to add game score ", error); throw error; } }; - async function completeGame(client, body) { + async function completeGame(client, body) { try { let log = JSON.parse(body); let rawScore = Number(log.score); @@ -184,11 +210,11 @@ export const handler = async (event, context) => { maxScore = Math.max(0, Math.round(maxScore)); } return await client` - UPDATE GAMES + UPDATE GAME_SCORES SET score = ${normalizedScore}, raw_correct = ${rawCorrect}, max_score = ${maxScore} - WHERE game_id = ${log.gameId};`; + WHERE game = ${log.gameId} AND student_id = ${log.studentId};`; } catch (error) { console.error("Failed to update score ", error); diff --git a/LearningLens2025/lambda/reflections/index.mjs b/LearningLens2025/lambda/reflections/index.mjs new file mode 100644 index 00000000..9e7dbbf9 --- /dev/null +++ b/LearningLens2025/lambda/reflections/index.mjs @@ -0,0 +1,160 @@ +import { DsqlSigner } from "@aws-sdk/dsql-signer"; +import postgres from "postgres" + +export const handler = async (event, context) => { + const signer = new DsqlSigner({ + hostname: process.env.AWS_DB_CLUSTER, + region: process.env.AWS_REGION, + }); + let client; + try { + // Use `getDbConnectAuthToken` if you are _not_ logging in as the `admin` user + const token = await signer.getDbConnectAdminAuthToken(); + + client = postgres({ + host: process.env.AWS_DB_CLUSTER, + user: "admin", + password: token, + database: "postgres", + port: 5432, + idle_timeout: 2, + ssl: { + rejectUnauthorized: true, + }, + }); + + } catch (error) { + console.error("Failed to create connection: ", error); + throw error; + } + const command = event["queryStringParameters"]["command"]; + const method = event["requestContext"]["http"]["method"]; + console.log(command); + + if (method === "GET") { + if (command === "getReflection") { + const courseId = BigInt(event["queryStringParameters"]["courseId"]); + const assignmentId = BigInt(event["queryStringParameters"]["assignmentId"]); + const lms = parseInt(event["queryStringParameters"]["lmsType"]); + return await getReflectionForAssignment(client, courseId, assignmentId, lms); + } + if (command === "getCompletedReflection") { + const reflectionId = event["queryStringParameters"]["reflectionId"]; + const studentId = BigInt(event["queryStringParameters"]["studentId"]); + return await getReflectionForSubmission(client, reflectionId, studentId); + } + } + if (method === "POST") { + if (command === "createDb") { + await buildDatabase(client); + return "Database created successfully."; + } + if (command === "completeReflection") { + await completeReflection(client, event["body"]); + return "Completed reflection successfully."; + } + if (command === "createReflection") { + await createReflection(client, event["body"]); + return "Added reflection successfully"; + } + } +}; + + async function buildDatabase(client) { + try { + await client`CREATE TABLE IF NOT EXISTS REFLECTIONS ( + reflection_id UUID PRIMARY KEY, + course_id BIGINT, + assignment_id BIGINT, + question VARCHAR, + date TIMESTAMP, + lms_service SMALLINT + );`; + await client`CREATE TABLE IF NOT EXISTS REFLECTION_RESPONSES ( + response_id UUID PRIMARY KEY, + student_id BIGINT, + response VARCHAR, + date TIMESTAMP, + reflection UUID + );` + } + catch (error) { + console.error("Failed to create database table: ", error); + throw error; + } +}; + + async function getReflectionForAssignment(client, courseId, assignmentId, lms) { + try { + return await client`SELECT reflection_id, question FROM REFLECTIONS WHERE + course_id = ${courseId} AND + assignment_id = ${assignmentId} AND + lms_service = ${lms};`; + } + catch (error) { + console.error("Failed to get reflections for assignment ", error); + throw error; + } + }; + + async function getReflectionForSubmission(client, reflectionId, studentId) { + try { + return await client`SELECT response_id, response, reflection FROM REFLECTION_RESPONSES WHERE + reflection = ${reflectionId} AND + student_id = ${studentId};`; + } + catch (error) { + console.error("Failed to get reflection for assignment ", error); + throw error; + } + }; + + async function createReflection(client, body) { + try { + let log = JSON.parse(body); + return await client` + INSERT INTO REFLECTIONS ( + reflection_id, + course_id, + assignment_id, + question, + date, + lms_service + ) VALUES ( + gen_random_uuid(), + ${log.courseId}, + ${log.assignmentId}, + ${log.question}, + current_timestamp AT TIME ZONE 'UTC', + ${log.lmsType} + );`; + } + catch (error) { + console.error("Failed to add reflection ", error); + throw error; + } + }; + + async function completeReflection(client, body) { + try { + let log = JSON.parse(body); + return await client` + INSERT INTO REFLECTION_RESPONSES ( + response_id, + student_id, + response, + date, + reflection + ) VALUES ( + gen_random_uuid(), + ${log.studentId}, + ${log.response}, + current_timestamp AT TIME ZONE 'UTC', + ${log.reflectionId} + );`; + } + catch (error) { + console.error("Failed to update reflection ", error); + throw error; + } + }; diff --git a/LearningLens2025/lambda/reflections/package-lock.json b/LearningLens2025/lambda/reflections/package-lock.json new file mode 100644 index 00000000..eb7c533b --- /dev/null +++ b/LearningLens2025/lambda/reflections/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "reflections", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "postgres": "^3.4.7" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + } + } +} diff --git a/LearningLens2025/lambda/reflections/package.json b/LearningLens2025/lambda/reflections/package.json new file mode 100644 index 00000000..2d5ce5c6 --- /dev/null +++ b/LearningLens2025/lambda/reflections/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "postgres": "^3.4.7" + } +} diff --git a/LearningLens2025/terraform/main.tf b/LearningLens2025/terraform/main.tf index 747c7668..bd1aaa3d 100644 --- a/LearningLens2025/terraform/main.tf +++ b/LearningLens2025/terraform/main.tf @@ -200,7 +200,7 @@ resource "aws_dsql_cluster" "edulense" { deletion_protection_enabled = true tags = { - Name = "EduLenseAILoggingDatabase" + Name = "EduLenseDatabaseCluster" } } @@ -244,25 +244,25 @@ resource "aws_iam_role_policy_attachment" "logging" { } # Run npm install before zipping -resource "null_resource" "npm_install_gettoken" { +resource "null_resource" "npm_install_ai_log" { triggers = { # Trigger npm install when package.json or lock file changes - package_json = filemd5("../lambda/gettoken/package.json") - package_lock = filemd5("../lambda/gettoken/package-lock.json") + package_json = filemd5("../lambda/ai_log/package.json") + package_lock = filemd5("../lambda/ai_log/package-lock.json") } provisioner "local-exec" { - command = "cd ../lambda/gettoken && npm install" + command = "cd ../lambda/ai_log && npm install" } } -data "archive_file" "get_token" { +data "archive_file" "ai_log" { type = "zip" - source_dir = "../lambda/gettoken" - excludes = ["../lambda/gettoken/gettoken.zip"] - output_path = "../lambda/gettoken/gettoken.zip" + source_dir = "../lambda/ai_log" + excludes = ["../lambda/ai_log/ai_log.zip"] + output_path = "../lambda/ai_log/ai_log.zip" - depends_on = [null_resource.npm_install_gettoken] + depends_on = [null_resource.npm_install_ai_log] } data "archive_file" "zip_plugin" { @@ -271,12 +271,12 @@ data "archive_file" "zip_plugin" { output_path = "../MoodlePlugin/learninglens.zip" } -resource "aws_lambda_function" "get_token" { - filename = data.archive_file.get_token.output_path - function_name = "get_db_token" +resource "aws_lambda_function" "ai_log" { + filename = data.archive_file.ai_log.output_path + function_name = "ai_log" role = aws_iam_role.lambda_token.arn handler = "index.handler" - source_code_hash = data.archive_file.get_token.output_base64sha256 + source_code_hash = data.archive_file.ai_log.output_base64sha256 runtime = "nodejs20.x" timeout = "10" environment { @@ -288,8 +288,8 @@ resource "aws_lambda_function" "get_token" { } } -resource "aws_lambda_function_url" "get_token_url" { - function_name = aws_lambda_function.get_token.function_name +resource "aws_lambda_function_url" "get_ai_log_url" { + function_name = aws_lambda_function.ai_log.function_name authorization_type = "NONE" cors { allow_methods = ["GET", "POST"] @@ -345,4 +345,53 @@ resource "aws_lambda_function_url" "get_game_data_url" { allow_origins = ["*"] allow_headers = ["content-type"] } +} + +# Run npm install before zipping +resource "null_resource" "npm_install_reflections" { + triggers = { + # Trigger npm install when package.json or lock file changes + package_json = filemd5("../lambda/reflections/package.json") + package_lock = filemd5("../lambda/reflections/package-lock.json") + } + + provisioner "local-exec" { + command = "cd ../lambda/reflections && npm install" + } +} + +data "archive_file" "reflections" { + type = "zip" + source_dir = "../lambda/reflections" + excludes = ["../lambda/reflections/reflections.zip"] + output_path = "../lambda/reflections/reflections.zip" + + depends_on = [null_resource.npm_install_reflections] +} + +resource "aws_lambda_function" "reflections" { + filename = data.archive_file.reflections.output_path + function_name = "reflections" + role = aws_iam_role.lambda_token.arn + handler = "index.handler" + source_code_hash = data.archive_file.reflections.output_base64sha256 + runtime = "nodejs20.x" + timeout = "10" + environment { + variables = { + ENVIRONMENT = "production" + LOG_LEVEL = "info" + AWS_DB_CLUSTER = format("%s.dsql.%s.on.aws", aws_dsql_cluster.edulense.identifier, data.aws_region.current.region) + } + } +} + +resource "aws_lambda_function_url" "get_reflections_url" { + function_name = aws_lambda_function.reflections.function_name + authorization_type = "NONE" + cors { + allow_methods = ["GET", "POST"] + allow_origins = ["*"] + allow_headers = ["content-type"] + } } \ No newline at end of file