diff --git a/team_a/teamA/lib/Api/llm/llm_api_modules_base.dart b/team_a/teamA/lib/Api/llm/llm_api_modules_base.dart index bbb6bdf9..1a420f89 100644 --- a/team_a/teamA/lib/Api/llm/llm_api_modules_base.dart +++ b/team_a/teamA/lib/Api/llm/llm_api_modules_base.dart @@ -13,5 +13,8 @@ abstract class LLM { LLM(this.apiKey); + // Abstract method that subclasses must implement Future generate(String prompt); + + } diff --git a/team_a/teamA/lib/Api/llm/perplexity_api.dart b/team_a/teamA/lib/Api/llm/perplexity_api.dart index 49aae50a..6e5860a8 100644 --- a/team_a/teamA/lib/Api/llm/perplexity_api.dart +++ b/team_a/teamA/lib/Api/llm/perplexity_api.dart @@ -3,10 +3,10 @@ import 'dart:convert'; import 'package:learninglens_app/Api/llm/llm_api_modules_base.dart'; import 'package:learninglens_app/services/api_service.dart'; -class PerplexityLLM implements LLM -{ +class PerplexityLLM implements LLM { @override final String apiKey; + @override final String url = 'https://api.perplexity.ai/chat/completions'; @override @@ -14,14 +14,12 @@ class PerplexityLLM implements LLM PerplexityLLM(this.apiKey); - Map convertHttpRespToJson(String httpResponseString) - { + Map convertHttpRespToJson(String httpResponseString) { return (json.decode(httpResponseString) as Map); } // - String getPostBody(String queryMessage) - { + String getPostBody(String queryMessage) { return jsonEncode({ // 'model': 'llama-3-sonar-large-32k-online', //'model': 'llama-3.1-sonar-large-128k-chat', @@ -34,8 +32,7 @@ class PerplexityLLM implements LLM } // - Map getPostHeaders() - { + Map getPostHeaders() { return ({ 'accept': 'application/json', 'content-type': 'application/json', @@ -44,12 +41,11 @@ class PerplexityLLM implements LLM } // - Uri getPostUrl() => Uri.https(this.url); + Uri getPostUrl() => Uri.https('api.perplexity.ai', '/chat/completions'); // Future postMessage( - Uri url, Map postHeaders, Object postBody) async - { + Uri url, Map postHeaders, Object postBody) async { final httpPackageResponse = await ApiService().httpPost(url, headers: postHeaders, body: postBody); @@ -64,8 +60,7 @@ class PerplexityLLM implements LLM return httpPackageResponse.body; } - List parseQueryResponse(String resp) - { + List parseQueryResponse(String resp) { // ignore: prefer_adjacent_string_concatenation String quizRegExp = // r'(<\?xml.*?\?>\s*(\s*.*?\s*.*?\s*(.*?)\s*.*?(\s*.*?)+\s*\s*.*?\s*(.*?)\s*.*?)+\s*)'; @@ -92,8 +87,7 @@ class PerplexityLLM implements LLM } // - Future postToLlm(String queryPrompt) async - { + Future postToLlm(String queryPrompt) async { var resp = ""; // use the following test query so Perplexity doesn't charge @@ -105,8 +99,7 @@ class PerplexityLLM implements LLM } // - Future queryAI(String query) async - { + Future queryAI(String query) async { final postHeaders = getPostHeaders(); final postBody = getPostBody(query); final httpPackageUrl = getPostUrl(); @@ -126,14 +119,14 @@ class PerplexityLLM implements LLM } Future getChatResponse(String prompt) async { - final postHeaders = getPostHeaders(); final postBody = getPostBody(prompt); final httpPackageUrl = getPostUrl(); try { // Make the POST request to the chat completions endpoint - var response = await ApiService().httpPost(httpPackageUrl, headers: postHeaders, body: postBody); + var response = await ApiService() + .httpPost(httpPackageUrl, headers: postHeaders, body: postBody); // Check for successful response if (response.statusCode == 200) { @@ -152,17 +145,16 @@ class PerplexityLLM implements LLM return 'An error occurred. Please check your internet connection and try again.'; } } - + @override Future generate(String prompt) async { print('Generating response for prompt Perplexity: $prompt'); - final postHeaders = getPostHeaders(); + final postHeaders = getPostHeaders(); final postBody = getPostBody(prompt); final url = getPostUrl(); final responseString = await postMessage(url, postHeaders, postBody); final responseJson = jsonDecode(responseString); return responseJson['choices'][0]['message']['content'].trim(); } - } diff --git a/team_a/teamA/lib/Api/lms/moodle/moodle_lms_service.dart b/team_a/teamA/lib/Api/lms/moodle/moodle_lms_service.dart index 2f04ecbb..13c8c4ec 100644 --- a/team_a/teamA/lib/Api/lms/moodle/moodle_lms_service.dart +++ b/team_a/teamA/lib/Api/lms/moodle/moodle_lms_service.dart @@ -69,9 +69,14 @@ class MoodleLmsService implements LmsInterface { Future login(String username, String password, String baseURL) async { print('Logging in to Moodle...'); + final body = { + 'username': username, + 'password': password, + 'service': 'moodle_mobile_app' + }; + // 1) Obtain the token by calling Moodle's login/token.php - final response = await ApiService().httpGet(Uri.parse( - '$baseURL/login/token.php?username=$username&password=$password&service=moodle_mobile_app')); + final response = await ApiService().httpPost(Uri.parse('$baseURL/login/token.php'), body: body); if (response.statusCode != 200) { throw HttpException(response.body); diff --git a/team_a/teamA/lib/Controller/custom_appbar.dart b/team_a/teamA/lib/Controller/custom_appbar.dart index b212a208..2d1450fb 100644 --- a/team_a/teamA/lib/Controller/custom_appbar.dart +++ b/team_a/teamA/lib/Controller/custom_appbar.dart @@ -29,6 +29,8 @@ class CustomAppBar extends StatefulWidget implements PreferredSizeWidget { class _CustomAppBarState extends State { @override Widget build(BuildContext context) { + final bool canAccessApp = canUserAccessApp(context); + return AppBar( backgroundColor: Theme.of(context).colorScheme.primaryContainer, title: Text( @@ -81,7 +83,9 @@ class _CustomAppBarState extends State { Flexible( child: IconButton( icon: Icon(Icons.science), // Science Icon - onPressed: () { + onPressed: !canAccessApp + ? null + : () { Navigator.push( context, MaterialPageRoute(builder: (context) => TextBasedFunctionCallerView()), @@ -192,4 +196,17 @@ class _CustomAppBarState extends State { ), ); } + + + bool canUserAccessApp(BuildContext context) { + return LocalStorageService.canUserAccessApp(); + } + + String getClassroom() { + return LocalStorageService.getClassroom(); + } + + bool isMoodle() { + return LocalStorageService.isMoodle(); + } } diff --git a/team_a/teamA/lib/Views/about_page.dart b/team_a/teamA/lib/Views/about_page.dart index c7f9822b..6ffb7816 100644 --- a/team_a/teamA/lib/Views/about_page.dart +++ b/team_a/teamA/lib/Views/about_page.dart @@ -58,7 +58,7 @@ class _TemplateState extends State{ Padding( padding: EdgeInsets.symmetric(horizontal: 16.0), // Adds left and right padding child: Text( - 'Learning Lens is an application developed by students at the University of Maryland Global Campus in the SWEN 670 Software Engineering Capstone course. It originated in the Fall 2024 student cohort and has been further developed by the students of Spring 2025. Some features and ideas were also developed from an application developed by a Fall 2024 cohort team named EvaluAI.\n\nLearning Lens is intended to be used by educators who teach students who utilize Learning Management Systems (LMS) like Moodle and Google Classroom. The application allows teachers to automatically generate quizzes, essay assignments, and lesson plans using various Artificial Intelligence platforms. There are also added features for Individual Education Plans and advanced analytics.\n\nSpring 2025 Contributors Under Team Name "EduLense": Nathaniel Boyd, Daniel Diep, Dinesh Ghimire, Andrew Hammes, Dusty McKinnon, Derek Sappington, and Kevin Watts', + 'Learning Lens is an application developed by students at the University of Maryland Global Campus in the SWEN 670 Software Engineering Capstone course. It originated in the Fall 2024 student cohort and has been further developed by the students of Spring 2025. Some features and ideas were also developed from an application developed by a Fall 2024 cohort team named EvaluAI.\n\nLearning Lens is intended to be used by educators who teach students who utilize Learning Management Systems (LMS) like Moodle and Google Classroom. The application allows teachers to automatically generate quizzes, essay assignments, and lesson plans using various Artificial Intelligence platforms. There are also added features for Individual Education Plans and advanced analytics.\n\nSpring 2025 Contributors Under Team Name "EduLense": Nathaniel Boyd, Daniel Diep, Dinesh Ghimire, Andrew Hammes, Dusty McKinnon, Derek Sappington, and Kevin Watts\n\nFall 2024 Contributors: Getinet Aga, Alexander Daugherty, Camille De Jesus, Desmond Herring, Jason Martin, Teja Tammali, Adam Williams, Scott McGlynn, Safia Azhar, Joneice Butler, Anthony Ohiosikha, Daanish Siddiqui, Conor Moore, and David Worthington\n\nSummer 2024 Contributors: Eric Bennett, George Gaynor, Nicholas Jungmarker, Syrone Robinson, Marsha Sapp, Henok Sibhatu, Tianming Zhu, Edward Shin, Jordan Gilberg, Mohammed Ghauri, Najwan Ismail, Stephen Buley, Whitney Meulink, and William Crowdus\n\nSpring 2024 Contributors: Tim Deering, Hemantha Adiga Madiyara, Colisian McLeod, Iriafen Ohiosikha, Nick Patton, Kathryn Scearce, Malaika Shell, Rene Wong', style: TextStyle(fontSize: 15), ), ), diff --git a/team_a/teamA/lib/Views/chat_screen.dart b/team_a/teamA/lib/Views/chat_screen.dart index 6b2f63ff..0136ed6d 100644 --- a/team_a/teamA/lib/Views/chat_screen.dart +++ b/team_a/teamA/lib/Views/chat_screen.dart @@ -52,6 +52,7 @@ class _ChatScreenState extends State { // Function to handle user message sending and API response Future _sendMessage() async { final input = _controller.text; + final aiPrompt = "$input IMPORTANT: Do not use any Markdown syntax (e.g., #, *, **, etc.). Use plain text only."; if (input.isEmpty) { return; @@ -83,7 +84,7 @@ class _ChatScreenState extends State { // Get ChatGPT response // final chatGPTService = OpenAiLLM(); final prompt = - _role == 'teacher' ? "You are assisting a teacher. $input" : input; + _role == 'teacher' ? "You are assisting a teacher. $aiPrompt" : aiPrompt; final response = await aiModel.getChatResponse(prompt); setState(() { diff --git a/team_a/teamA/lib/Views/dashboard.dart b/team_a/teamA/lib/Views/dashboard.dart index d80cc950..39b9b2b1 100644 --- a/team_a/teamA/lib/Views/dashboard.dart +++ b/team_a/teamA/lib/Views/dashboard.dart @@ -76,18 +76,15 @@ class TeacherDashboard extends StatelessWidget { } bool canUserAccessApp(BuildContext context) { - bool isLoggedIntoGoogleClassroom = LocalStorageService.isLoggedIntoGoogle() && LocalStorageService.hasLLMKey(); - bool isLoggedIntoMoodle = LocalStorageService.isLoggedIntoMoodle() && LocalStorageService.hasLLMKey(); - return isMoodle() ? isLoggedIntoMoodle : isLoggedIntoGoogleClassroom; + return LocalStorageService.canUserAccessApp(); } String getClassroom() { - return LocalStorageService.getSelectedClassroom() == LmsType.MOODLE ? 'Moodle' : 'Google'; + return LocalStorageService.getClassroom(); } bool isMoodle() { - print(LocalStorageService.getSelectedClassroom()); - return LocalStorageService.getSelectedClassroom() == LmsType.MOODLE; + return LocalStorageService.isMoodle(); } Widget _buildDesktopLayout(BuildContext context, BoxConstraints constraints) { diff --git a/team_a/teamA/lib/Views/essay_generation.dart b/team_a/teamA/lib/Views/essay_generation.dart index 5391c39a..0c1c3118 100644 --- a/team_a/teamA/lib/Views/essay_generation.dart +++ b/team_a/teamA/lib/Views/essay_generation.dart @@ -123,7 +123,7 @@ class _EssayGenerationState extends State String queryPrompt = ''' I am building a program that creates rubrics when provided with assignment information. I will provide you with the following information about the assignment that needs a rubric: Difficulty level, point scale, assignment objective, assignment description. You may also receive additional customization rules. - Using this information, you will reply with a rubric that includes 3-5 criteria. Your reply must only contain the JSON information, and begin with a {. + Using this information, you will reply with a rubric that includes 4 criteria. Your reply must only contain the JSON information, and begin with a {. Remove any ``` from your output. You must reply with a representation of the rubric in JSON format that exactly matches this format: diff --git a/team_a/teamA/lib/Views/g_lesson_plan.dart b/team_a/teamA/lib/Views/g_lesson_plan.dart index 1c96a21a..386ca5a7 100644 --- a/team_a/teamA/lib/Views/g_lesson_plan.dart +++ b/team_a/teamA/lib/Views/g_lesson_plan.dart @@ -179,11 +179,16 @@ class _LessonPlanState extends State { } String prompt = - "Create a concise, all-text lesson plan for ${lessonPlanNameController.text} for grade ${selectedGradeLevel == 'K' ? 'Kindergarten' : selectedGradeLevel} covering ${manualEntryController.text}. ${aiPromptDetailsController.text}. Write it as student-facing content for studying, essays, and quizzes. Use plain text, no Markdown, in 500 words."; - + """Generate an all text (no diagrams) lesson of less than 500 words for ${lessonPlanNameController.text} for grade $selectedGradeLevel covering key topics like ${manualEntryController.text}. ${aiPromptDetailsController.text}. This lesson is WHAT THE STUDENT WILL SEE! This lesson will be viewed by students and students will use it to study from (which will help them write essays and take quizzes). IMPORTANT: Do not use any Markdown syntax (e.g., #, *, **, etc.). Use plain text only."""; var result = await aiModel.generate(prompt); + print('Generated lesson plan before cleaning: $result'); + + // Strip any Markdown from the result + String cleanedResult = stripMarkdown(result); + print('Generated lesson plan after cleaning: $cleanedResult'); + setState(() { - manualEntryController.text = result; + manualEntryController.text = cleanedResult; }); } catch (e) { log.severe("Error generating lesson plan: $e"); @@ -197,6 +202,20 @@ class _LessonPlanState extends State { } } + String stripMarkdown(String text) { + // Remove common Markdown syntax + String cleanedText = text + .replaceAll(RegExp(r'#+\s'), '') // Remove headers (e.g., #, ##) + .replaceAll(RegExp(r'\*\*'), '') // Remove bold (**) + .replaceAll(RegExp(r'\*'), '') // Remove italics or list markers (*) + .replaceAll(RegExp(r'_'), '') // Remove italics or emphasis (_) + .replaceAll(RegExp(r'```[\s\S]*?```'), '') // Remove code blocks + .replaceAll(RegExp(r'`'), '') // Remove inline code + .replaceAll(RegExp(r'-\s'), '') // Remove list markers (-) + .replaceAll(RegExp(r'>\s'), ''); // Remove blockquotes (>) + return cleanedText.trim(); + } + void _showLessonPlanDialog(dynamic lessonPlan) { showDialog( context: context, diff --git a/team_a/teamA/lib/Views/g_quiz_question_page.dart b/team_a/teamA/lib/Views/g_quiz_question_page.dart index 7e8f7af9..d3974606 100644 --- a/team_a/teamA/lib/Views/g_quiz_question_page.dart +++ b/team_a/teamA/lib/Views/g_quiz_question_page.dart @@ -260,7 +260,7 @@ class _QuizQuestionPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar( - title: 'Quiz Questions Here ....', + title: 'Quiz Questions', userprofileurl: LmsFactory.getLmsService().profileImage ?? '', ), body: FutureBuilder( diff --git a/team_a/teamA/lib/Views/send_essay_to_moodle.dart b/team_a/teamA/lib/Views/send_essay_to_moodle.dart index 65d567b3..4ff0b829 100644 --- a/team_a/teamA/lib/Views/send_essay_to_moodle.dart +++ b/team_a/teamA/lib/Views/send_essay_to_moodle.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; 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'; @@ -267,7 +268,11 @@ class EssayAssignmentSettingsState extends State { labelText: 'Section Number', border: OutlineInputBorder(), ), - // Adding validator to ensure assignment name is not empty + keyboardType: TextInputType.number, // Set keyboard type to number + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly // Allow only digits + ], + // Adding validator to ensure section number is not empty validator: (value) { if (value == null || value.isEmpty) { return 'Please enter a section number'; diff --git a/team_a/teamA/lib/Views/view_submissions.dart b/team_a/teamA/lib/Views/view_submissions.dart index 0ed3eca7..4e09dd33 100644 --- a/team_a/teamA/lib/Views/view_submissions.dart +++ b/team_a/teamA/lib/Views/view_submissions.dart @@ -44,7 +44,7 @@ class SubmissionListState extends State { LlmType? selectedLLM; - String filterOption = 'All'; + String filterOption = 'All Students'; String fullNameFilter = ''; String getApiKey(LlmType selectedLLM) { @@ -139,10 +139,11 @@ class SubmissionListState extends State { child: Row( children: [ Expanded( - child: DropdownButton( + child: DropdownButtonFormField( value: filterOption, + decoration: InputDecoration(labelText: 'Submission Status'), onChanged: _handleFilterChanged, - items: ['All', 'With Submission', 'Without Submission'] + items: ['All Students', 'With Submission', 'Without Submission'] .map>((String value) { return DropdownMenuItem( value: value, diff --git a/team_a/teamA/lib/beans/quiz.dart b/team_a/teamA/lib/beans/quiz.dart index e05ef45e..95e69620 100644 --- a/team_a/teamA/lib/beans/quiz.dart +++ b/team_a/teamA/lib/beans/quiz.dart @@ -49,15 +49,57 @@ class Quiz { return quiz; } + // JSON factory constructor using JSON map + static Quiz fromGoogleJson(Map json) { Quiz tmpQuiz = Quiz(); - tmpQuiz.name = json['title']; - tmpQuiz.description = json['description']; - tmpQuiz.questionList = []; - tmpQuiz.promptUsed = ''; - tmpQuiz.id = int.parse(json['id']); - tmpQuiz.coursedId = int.parse(json['courseId']); - + + print('Debug: Parsing JSON: $json'); + + // Basic fields + tmpQuiz.name = json['title'] as String? ?? ''; + print('Debug: Name set to: ${tmpQuiz.name}'); + + tmpQuiz.description = json['description'] as String? ?? ''; + print('Debug: Description set to: ${tmpQuiz.description}'); + + tmpQuiz.questionList = []; + tmpQuiz.promptUsed = ''; + print('Debug: Initialized questionList and promptUsed'); + + // Parse IDs + final idStr = json['id']?.toString() ?? '0'; + tmpQuiz.id = int.tryParse(idStr) ?? 0; + print('Debug: ID parsed from "$idStr" to: ${tmpQuiz.id}'); + + final courseIdStr = json['courseId']?.toString() ?? '0'; + tmpQuiz.coursedId = int.tryParse(courseIdStr) ?? 0; + print( + 'Debug: CourseID parsed from "$courseIdStr" to: ${tmpQuiz.coursedId}'); + + // Parse creation time as open time + final creationTimeStr = json['creationTime']?.toString() ?? ''; + tmpQuiz.timeOpen = DateTime.tryParse(creationTimeStr) ?? DateTime.now(); + print( + 'Debug: CreationTime parsed from "$creationTimeStr" to: ${tmpQuiz.timeOpen}'); + + // Parse due date (which is an object with year, month, day) + DateTime dueDateTime = DateTime.now(); + try { + final dueDate = json['dueDate'] as Map?; + if (dueDate != null) { + final year = dueDate['year'] as int? ?? DateTime.now().year; + final month = dueDate['month'] as int? ?? DateTime.now().month; + final day = dueDate['day'] as int? ?? DateTime.now().day; + dueDateTime = DateTime(year, month, day); + } + } catch (e) { + print('Debug: Error parsing dueDate, using default: $e'); + } + tmpQuiz.timeClose = dueDateTime; + print('Debug: DueDate parsed to: ${tmpQuiz.timeClose}'); + + print('Debug: Quiz object created successfully'); return tmpQuiz; } diff --git a/team_a/teamA/lib/notifiers/login_notifier.dart b/team_a/teamA/lib/notifiers/login_notifier.dart index d10203ff..a4dc8204 100644 --- a/team_a/teamA/lib/notifiers/login_notifier.dart +++ b/team_a/teamA/lib/notifiers/login_notifier.dart @@ -97,17 +97,25 @@ class LoginNotifier with ChangeNotifier { await _api.login(username, password, moodleUrl); if (_api.isLoggedIn()) { - _moodleState.isLoggedIn = true; - _moodleState.errorMessage = null; // Clear any old error - _username = username; - _password = password; - _moodleUrl = moodleUrl; - - // Save to local storage - LocalStorageService.saveMoodleLoginState(_moodleState.isLoggedIn); - LocalStorageService.saveCredentials(username, password); - LocalStorageService.saveMoodleUrl(moodleUrl); - + // Make sure moodle user is a teacher. + if (await _api.isUserTeacher(_api.courses!)) { + // User is a teacher + _moodleState.isLoggedIn = true; + _moodleState.errorMessage = null; // Clear any old error + _username = username; + _password = password; + _moodleUrl = moodleUrl; + + // Save to local storage + LocalStorageService.saveMoodleLoginState(_moodleState.isLoggedIn); + LocalStorageService.saveCredentials(username, password); + LocalStorageService.saveMoodleUrl(moodleUrl); + } else { + // user is not a teacher + _api.logout(); + _moodleState.isLoggedIn = false; + _moodleState.errorMessage = "User is not a teacher"; + } } else { // Logged in is false; set a custom error _moodleState.isLoggedIn = false; diff --git a/team_a/teamA/lib/services/local_storage_service.dart b/team_a/teamA/lib/services/local_storage_service.dart index cbbea01e..1abb5c1d 100644 --- a/team_a/teamA/lib/services/local_storage_service.dart +++ b/team_a/teamA/lib/services/local_storage_service.dart @@ -237,4 +237,19 @@ class LocalStorageService { return false; } + + static bool canUserAccessApp() { + bool isLoggedIntoGoogleClassroom = LocalStorageService.isLoggedIntoGoogle() && LocalStorageService.hasLLMKey(); + bool isLoggedIntoMoodle = LocalStorageService.isLoggedIntoMoodle() && LocalStorageService.hasLLMKey(); + return isMoodle() ? isLoggedIntoMoodle : isLoggedIntoGoogleClassroom; + } + + static String getClassroom() { + return LocalStorageService.getSelectedClassroom() == LmsType.MOODLE ? 'Moodle' : 'Google'; + } + + static bool isMoodle() { + print(LocalStorageService.getSelectedClassroom()); + return LocalStorageService.getSelectedClassroom() == LmsType.MOODLE; + } }