diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 00000000..7dd880b5 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "spring2025-81f5b" + } +} diff --git a/.gitignore b/.gitignore index 3a83c2f0..a0883058 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..a91a9154 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,94 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Flutter", + "type": "dart", + "request": "launch", + "program": "lib/main.dart" + }, + { + "name": "team_a", + "cwd": "team_a", + "request": "launch", + "type": "dart" + }, + { + "name": "team_a (profile mode)", + "cwd": "team_a", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "team_a (release mode)", + "cwd": "team_a", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "stml_application", + "cwd": "STML\\stml_application", + "request": "launch", + "type": "dart" + }, + { + "name": "stml_application (profile mode)", + "cwd": "STML\\stml_application", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "stml_application (release mode)", + "cwd": "STML\\stml_application", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "teamA", + "cwd": "team_a\\teamA", + "request": "launch", + "type": "dart" + }, + { + "name": "teamA (profile mode)", + "cwd": "team_a\\teamA", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "teamA (release mode)", + "cwd": "team_a\\teamA", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "yappy", + "cwd": "team_b\\yappy", + "request": "launch", + "type": "dart" + }, + { + "name": "yappy (profile mode)", + "cwd": "team_b\\yappy", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "yappy (release mode)", + "cwd": "team_b\\yappy", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..88550be3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "cmake.ignoreCMakeListsMissing": true, + "dart.flutterSdkPath": "C:\\flutter_windows_3.29.0-stable\\flutter" +} \ No newline at end of file diff --git a/STML/stml_application/android/app/build.gradle b/STML/stml_application/android/app/build.gradle index a7b7327d..52e942fe 100644 --- a/STML/stml_application/android/app/build.gradle +++ b/STML/stml_application/android/app/build.gradle @@ -9,7 +9,7 @@ plugins { android { namespace = "com.umgc.memoryminder" compileSdk = 35 - ndkVersion = "27.0.12077973" + ndkVersion = "29.0.13113456" compileOptions { coreLibraryDesugaringEnabled true diff --git a/STML/stml_application/android/app/src/main/AndroidManifest.xml b/STML/stml_application/android/app/src/main/AndroidManifest.xml index 0064a8e8..c13b29df 100644 --- a/STML/stml_application/android/app/src/main/AndroidManifest.xml +++ b/STML/stml_application/android/app/src/main/AndroidManifest.xml @@ -1,14 +1,26 @@ + + + - - - + + + + + + + + + + + + sendHelpNotification() async { + // Remplacez 'caregiver_device_token' par le token du soignant + await _firebaseMessaging.sendMessage( + to: 'caregiver_device_token', + data: { + 'type': 'help_request', + 'message': 'The STML user has requested help.', + }, + ); + } +} diff --git a/STML/stml_application/lib/services/notification_service.dart b/STML/stml_application/lib/services/notification_service.dart new file mode 100644 index 00000000..b88ef896 --- /dev/null +++ b/STML/stml_application/lib/services/notification_service.dart @@ -0,0 +1,249 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:cloud_functions/cloud_functions.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:memoryminder/ui/location_history_screen.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; + +class NotificationService { + final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; + final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + final FirebaseAnalytics _analytics = FirebaseAnalytics.instance; + + String? _caregiverToken; + String? _lastRequestId; + + String? get lastRequestId => _lastRequestId; + + Future initialize() async { + // Get the FCM token for the current device + _caregiverToken = await _firebaseMessaging.getToken(); + print("Caregiver Token: $_caregiverToken"); + + // Log token generation for analytics + await _analytics.logEvent( + name: 'fcm_token_generated', + parameters: { + 'user_type': 'caregiver', + }, + ); + + // Listen for new tokens (in case the token changes) + _firebaseMessaging.onTokenRefresh.listen((newToken) { + _caregiverToken = newToken; + print("New Caregiver Token: $_caregiverToken"); + + // Log token refresh for analytics + _analytics.logEvent( + name: 'fcm_token_refreshed', + parameters: { + 'user_type': 'caregiver', + }, + ); + }); + + // Configure local notifications + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('@mipmap/ic_launcher'); + final InitializationSettings initializationSettings = + InitializationSettings( + android: initializationSettingsAndroid, + ); + await _flutterLocalNotificationsPlugin.initialize(initializationSettings); + + // Listen for incoming messages + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + print("Message received: ${message.notification?.title}"); + _showNotification(message); + + // Log message received for analytics + _analytics.logEvent( + name: 'notification_received', + parameters: { + 'notification_type': message.data['type'] ?? 'unknown', + 'has_notification': message.notification != null, + }, + ); + }); + } + + Future _showNotification(RemoteMessage message) async { + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'your_channel_id', // Notification channel ID + 'your_channel_name', // Notification channel name + importance: Importance.max, + priority: Priority.high, + ); + const NotificationDetails platformChannelSpecifics = + NotificationDetails(android: androidPlatformChannelSpecifics); + + await _flutterLocalNotificationsPlugin.show( + 0, // Notification ID + message.notification?.title, // Notification title + message.notification?.body, // Notification body + platformChannelSpecifics, + ); + + // Log notification shown for analytics + await _analytics.logEvent( + name: 'notification_displayed', + parameters: { + 'notification_title': message.notification?.title ?? 'No title', + }, + ); + } + + Future sendHelpNotification(LocationEntry? currentLocationEntry) async { + try { + // Start timer for performance tracking + final startTime = DateTime.now(); + + // Prepare location data + Map? locationData; + + if (currentLocationEntry != null) { + try { + // Try to get the current coordinates + Position currentPosition = await Geolocator.getCurrentPosition(); + + // Create the data map with the stored address and current coordinates + locationData = { + 'address': currentLocationEntry.address, + 'latitude': currentPosition.latitude, + 'longitude': currentPosition.longitude, + 'timestamp': currentLocationEntry.startTime.toIso8601String() + }; + + // Log successful location capture + await _analytics.logEvent( + name: 'emergency_location_captured', + parameters: { + 'method': 'geolocator', + 'has_coordinates': true, + }, + ); + } catch (e) { + // Log location error + await _analytics.logEvent( + name: 'emergency_location_error', + parameters: { + 'error_type': 'geolocator_error', + 'error_message': e.toString(), + }, + ); + + // Fallback: try to geocode the address to get coordinates + try { + List locations = + await locationFromAddress(currentLocationEntry.address); + if (locations.isNotEmpty) { + locationData = { + 'address': currentLocationEntry.address, + 'latitude': locations.first.latitude, + 'longitude': locations.first.longitude, + 'timestamp': currentLocationEntry.startTime.toIso8601String() + }; + + // Log successful geocoding + await _analytics.logEvent( + name: 'emergency_location_captured', + parameters: { + 'method': 'geocoding', + 'has_coordinates': true, + }, + ); + } else { + // If geocoding fails, only send the address + locationData = { + 'address': currentLocationEntry.address, + 'timestamp': currentLocationEntry.startTime.toIso8601String() + }; + + // Log geocoding with no results + await _analytics.logEvent( + name: 'emergency_location_captured', + parameters: { + 'method': 'geocoding', + 'has_coordinates': false, + }, + ); + } + } catch (e) { + // Log geocoding error + await _analytics.logEvent( + name: 'emergency_location_error', + parameters: { + 'error_type': 'geocoding_error', + 'error_message': e.toString(), + }, + ); + + // As a last resort, only send the address + locationData = { + 'address': currentLocationEntry.address, + 'timestamp': currentLocationEntry.startTime.toIso8601String() + }; + } + } + } else { + // Log no location available + await _analytics.logEvent( + name: 'emergency_location_missing', + ); + } + + // Call the Cloud Function + final HttpsCallable callable = + FirebaseFunctions.instance.httpsCallable('sendHelpAlert'); + final result = await callable.call({ + 'caregiverToken': _caregiverToken, + 'location': locationData, + 'userId': FirebaseAuth.instance.currentUser?.uid, + 'userName': FirebaseAuth.instance.currentUser?.displayName + }); + + // Save the request ID for later reference + _lastRequestId = result.data['requestId']; + + // Calculate time taken + final endTime = DateTime.now(); + final duration = endTime.difference(startTime).inMilliseconds; + + // Create parameters map without nullable values + final Map analyticsParams = { + 'success': result.data['success'] ?? false, + 'has_location': currentLocationEntry != null, + 'processing_time_ms': duration, + }; + + // Add requestId only if it's not null + if (_lastRequestId != null) { + analyticsParams['request_id'] = _lastRequestId!; + } + + // Log complete event with success/failure + await _analytics.logEvent( + name: 'emergency_alert_sent', + parameters: analyticsParams, + ); + + return result.data['success'] ?? false; + } catch (e) { + print("Error sending notification: $e"); + + // Log error event + await _analytics.logEvent( + name: 'emergency_alert_error', + parameters: { + 'error_message': e.toString(), + }, + ); + + return false; + } + } +} diff --git a/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/caregiver-dashboard.dart b/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/caregiver-dashboard.dart index 26949d01..5a6dfd6e 100644 --- a/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/caregiver-dashboard.dart +++ b/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/caregiver-dashboard.dart @@ -10,10 +10,10 @@ import 'package:memoryminder/src/features/caregiver-dashboard/presentation/care_ import 'package:memoryminder/src/features/caregiver-dashboard/service/manage_care_recipient_service.dart'; import 'package:memoryminder/src/features/caregiver-dashboard/service/notification_service.dart'; import 'package:memoryminder/src/features/caregiver-dashboard/service/notification_stream_service.dart'; -import 'package:memoryminder/ui/profile_screen.dart'; import 'package:memoryminder/src/utils/ui_utils.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:url_launcher/url_launcher.dart'; // Main HomeScreen widget which is a stateless widget. class CaregiverDashboardScreen extends StatefulWidget { @@ -33,6 +33,15 @@ class _CaregiverDashboardScreen extends State { _careRecipientData = ManageCareRecipientService().getAllCareRecipients(); } + Future _callEmergencyNumber() async { + final Uri phoneUri = Uri.parse('tel:911'); + if (await canLaunchUrl(phoneUri)) { + await launchUrl(phoneUri); + } else { + throw 'Could not launch $phoneUri'; + } + } + @override void dispose() { NotificationStreamService().dispose(); @@ -47,7 +56,6 @@ class _CaregiverDashboardScreen extends State { appBar: const CustomAppBar( title: 'Caregiver Dashboard', ), - body: Container( /*decoration: BoxDecoration( image: DecorationImage( @@ -112,7 +120,6 @@ class _CaregiverDashboardScreen extends State { ), ), ), - const Divider( color: Colors.black54, thickness: 2, @@ -120,7 +127,33 @@ class _CaregiverDashboardScreen extends State { indent: 20, endIndent: 20, ), - + const Divider( + color: Colors.black54, + thickness: 2, + height: 10, + indent: 20, + endIndent: 20, + ), + ElevatedButton( + onPressed: _callEmergencyNumber, + style: ElevatedButton.styleFrom( + backgroundColor: + Colors.red, // Couleur rouge pour indiquer l'urgence + padding: + const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + ), + child: const Text( + 'Emergency Call', + style: TextStyle(fontSize: 18, color: Colors.white), + ), + ), + const Divider( + color: Colors.black54, + thickness: 2, + height: 10, + indent: 20, + endIndent: 20, + ), Padding( padding: EdgeInsets.fromLTRB(2.0, 2, 2.0, 2), child: Column( @@ -138,7 +171,8 @@ class _CaregiverDashboardScreen extends State { onPressed: () { Navigator.push( context, - MaterialPageRoute(builder: (context) => AddCareRecipientForm()), + MaterialPageRoute( + builder: (context) => AddCareRecipientForm()), ); }, icon: Icon( @@ -149,18 +183,17 @@ class _CaregiverDashboardScreen extends State { style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.black, - padding: EdgeInsets.fromLTRB(2.0, 2, 16.0, 2), // Apply padding here + padding: EdgeInsets.fromLTRB( + 2.0, 2, 16.0, 2), // Apply padding here ), ), ], ), ), - - Expanded( - child: _buildCareRecipientsGrid( - context: context, - ), + child: _buildCareRecipientsGrid( + context: context, + ), ), const Divider( color: Colors.black, @@ -187,61 +220,64 @@ class _CaregiverDashboardScreen extends State { final data = snapshot.data!; return GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, // Adjust as needed - childAspectRatio: 1.30, - mainAxisSpacing: 10, - crossAxisSpacing: 10 - ), + crossAxisCount: 3, // Adjust as needed + childAspectRatio: 1.0, + mainAxisSpacing: 3, + crossAxisSpacing: 3), itemCount: data.length, itemBuilder: (context, index) { final item = data[index]; - final String labelText = '${item['firstName'].toString()} ${item['lastName'].toString()}'; + final String labelText = + '${item['firstName'].toString()} ${item['lastName'].toString()}'; final careRecipient = CareRecipient.fromMap(item); - return InkWell( // Or InkWell for ripple effect - onTap: () { - // Handle item click - print('Item ${item['firstName'].toString()} clicked'); - Navigator.push( - context, - MaterialPageRoute(builder: (context) => CareRecipientProfileScreen(careRecipientId: item['itemId'].toString(), careRecipientData: careRecipient.toMap())), - ); - }, - borderRadius: BorderRadius.circular(12.0), - child: Container( - decoration: BoxDecoration( - color: Colors.lightBlue[100], - borderRadius: BorderRadius.circular(12.0), - boxShadow: [ - BoxShadow( - color: Colors.lightBlueAccent, - spreadRadius: 1, - blurRadius: 3, - offset: const Offset(0, 2), - ), - ], - ), - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.person, - size: 40.0, - color: Color.fromARGB(255, 2, 63, 129), - ), - const SizedBox(height: 8.0), - Text( - labelText, - textAlign: TextAlign.center, - style: const TextStyle( - fontWeight: FontWeight.w500, - color: Colors.black87, + return InkWell( + // Or InkWell for ripple effect + onTap: () { + // Handle item click + print('Item ${item['firstName'].toString()} clicked'); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CareRecipientProfileScreen( + careRecipientId: item['itemId'].toString(), + careRecipientData: careRecipient.toMap())), + ); + }, + borderRadius: BorderRadius.circular(12.0), + child: Container( + decoration: BoxDecoration( + color: Colors.lightGreen[100], + borderRadius: BorderRadius.circular(12.0), + boxShadow: [ + BoxShadow( + color: Colors.lightGreen.withOpacity(0.5), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 2), + ), + ], + ), + padding: const EdgeInsets.all(3.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.person, + size: 40.0, + color: Colors.green, + ), + const SizedBox(height: 8.0), + Text( + labelText, + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.w500, + color: Colors.black87, ), ), ], ), - ) - ); + )); }, ); } else if (snapshot.hasError) { @@ -300,7 +336,6 @@ class _CaregiverDashboardScreen extends State { notificationService .markNotificationAsRead(notification['id']); }, - ); }, )); @@ -308,5 +343,3 @@ class _CaregiverDashboardScreen extends State { ); } } - - diff --git a/STML/stml_application/lib/src/features/caregiver-dashboard/service/notification_service.dart b/STML/stml_application/lib/src/features/caregiver-dashboard/service/notification_service.dart index 2742da48..a2a93695 100644 --- a/STML/stml_application/lib/src/features/caregiver-dashboard/service/notification_service.dart +++ b/STML/stml_application/lib/src/features/caregiver-dashboard/service/notification_service.dart @@ -7,29 +7,34 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:memoryminder/src/features/caregiver-dashboard/service/notification_stream_service.dart'; - - +import 'package:url_launcher/url_launcher.dart'; class NotificationService { final FirebaseMessaging _messaging = FirebaseMessaging.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance; - final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); Future initialize() async { await _messaging.subscribeToTopic("STML_USER_PRESSED_HELP"); - _messaging.requestPermission(alert: true, + _messaging.requestPermission( + alert: true, announcement: true, badge: true, carPlay: false, criticalAlert: true, provisional: false, - sound: true,); + sound: true, + ); FirebaseMessaging.onMessage.listen((RemoteMessage message) async { - print('---------------------------------------------------------------------'); - print('---Received a notification message: ${message.notification?.title}---'); - print('---------------------------------------------------------------------'); + print( + '---------------------------------------------------------------------'); + print( + '---Received a notification message: ${message.notification?.title}---'); + print( + '---------------------------------------------------------------------'); _storeMessage(message); await Future.delayed(Duration(seconds: 5)); @@ -38,25 +43,43 @@ class NotificationService { }); FirebaseMessaging.onBackgroundMessage(_backgroundMessageHandler); - const AndroidInitializationSettings androidInitializationSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); - const DarwinInitializationSettings iOSinitializationSettings = DarwinInitializationSettings( + + const AndroidInitializationSettings androidInitializationSettings = + AndroidInitializationSettings('@mipmap/ic_launcher'); + const DarwinInitializationSettings iOSinitializationSettings = + DarwinInitializationSettings( requestAlertPermission: true, requestBadgePermission: true, requestSoundPermission: true, ); - const InitializationSettings initializationSettings = InitializationSettings(android: androidInitializationSettings, iOS: iOSinitializationSettings); - await flutterLocalNotificationsPlugin.initialize(initializationSettings); - + const InitializationSettings initializationSettings = + InitializationSettings( + android: androidInitializationSettings, + iOS: iOSinitializationSettings, + ); + await flutterLocalNotificationsPlugin.initialize( + initializationSettings, + onDidReceiveNotificationResponse: (NotificationResponse response) async { + if (response.actionId == 'emergency_call') { + const phoneNumber = 'tel:911'; + if (await canLaunch(phoneNumber)) { + await launch(phoneNumber); + } else { + throw 'Could not launch $phoneNumber'; + } + } + }, + ); // Handle initial message when the app is opened from a terminated state - RemoteMessage? initialMessage = await FirebaseMessaging.instance.getInitialMessage(); + RemoteMessage? initialMessage = + await FirebaseMessaging.instance.getInitialMessage(); if (initialMessage != null) { _handleMessage(initialMessage); } // Handle when the app is opened from background state. FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage); - } static void _handleMessage(RemoteMessage message) { @@ -65,72 +88,94 @@ class NotificationService { // Example: Navigator.of(navigatorKey.currentContext!).pushNamed('/details', arguments: message.data); } - - - Future _showLocalNotification(RemoteMessage message) async{ - const AndroidNotificationDetails androidChannelSpecifics = AndroidNotificationDetails('Help', 'Help', channelDescription: 'User pressed Help button', importance: Importance.max, priority: Priority.high, ticker: 'ticker'); - const NotificationDetails channelSpecifics = NotificationDetails(android: androidChannelSpecifics, iOS: const DarwinNotificationDetails()); - await flutterLocalNotificationsPlugin.show(0, message.notification?.title, message.notification?.title, channelSpecifics); - + Future _showLocalNotification(RemoteMessage message) async { + const AndroidNotificationDetails androidChannelSpecifics = + AndroidNotificationDetails( + 'Help', //Canal ID + 'Help Notifications', // canal name + channelDescription: + 'Notifications when the STML user presses the HELP button', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker', + enableVibration: true, //vibrations + playSound: true, // play tone + actions: [ + AndroidNotificationAction( + 'emergency_call', // action Id + 'Emergency Call', // action text + ), + ], + ); + const NotificationDetails channelSpecifics = NotificationDetails( + android: androidChannelSpecifics, + iOS: const DarwinNotificationDetails()); + await flutterLocalNotificationsPlugin.show(0, message.notification?.title, + message.notification?.body, channelSpecifics); } Future _storeMessage(RemoteMessage message) async { await _firestore.collection('notifications').add({ 'title': message.notification?.title, - 'read': false, //Flag to indicate if the message is read or not. + 'read': false, // Flag to indicate if the message is read or not. 'createdDate': FieldValue.serverTimestamp(), + 'stmlUserId': 'USER_ID', // Replaced by STML's userID }); } Future markNotificationAsRead(String messageId) async { try { - DocumentReference notificationRef = _firestore.collection('notifications').doc(messageId); - await notificationRef.update({'read':true}); + DocumentReference notificationRef = + _firestore.collection('notifications').doc(messageId); + await notificationRef.update({'read': true}); print('Notification updated as read'); getRecentNotifications(); - - } catch(e) { + } catch (e) { print('Error updating notification data: $e'); } } Future getRecentNotifications() async { try { - QuerySnapshot snapshot = await _firestore.collection('notifications') + QuerySnapshot snapshot = await _firestore + .collection('notifications') .orderBy('createdDate', descending: true) .limit(10) .get(); List> notifications = snapshot.docs.map((doc) { - return {'id': doc.id, + return { + 'id': doc.id, 'title': doc['title'], 'read': doc['read'], 'createDate': doc['createdDate'], }; }).toList(); NotificationStreamService().addData(notifications); - } catch(e) { + } catch (e) { print('Error fetching notifications $e'); } } -Future sendNotificationToFirestore(String title) async { - await FirebaseFirestore.instance.collection('notifications').add({ - 'title': title, - 'read': false, // Mark as unread by default - 'createdDate': FieldValue.serverTimestamp(), - }); - -} + Future sendNotificationToFirestore(String title) async { + await FirebaseFirestore.instance.collection('notifications').add({ + 'title': title, + 'read': false, // Mark as unread by default + 'createdDate': FieldValue.serverTimestamp(), + }); + } } Future _backgroundMessageHandler(RemoteMessage message) async { - print('---------------------------------------------------------------------'); - print('---Received a background data message----${message.notification?.title}---------------------------'); - print('---------------------------------------------------------------------'); + print( + '---------------------------------------------------------------------'); + print( + '---Received a background data message----${message.notification?.title}---------------------------'); + print( + '---------------------------------------------------------------------'); await Firebase.initializeApp(); NotificationService()._storeMessage(message); await Future.delayed(Duration(seconds: 5)); NotificationService().getRecentNotifications(); NotificationService()._showLocalNotification(message); -} \ No newline at end of file +} diff --git a/STML/stml_application/lib/src/features/stml_user_dashboard/presentation/stml_user_dashboard.dart b/STML/stml_application/lib/src/features/stml_user_dashboard/presentation/stml_user_dashboard.dart index ff208826..163c9bf4 100644 --- a/STML/stml_application/lib/src/features/stml_user_dashboard/presentation/stml_user_dashboard.dart +++ b/STML/stml_application/lib/src/features/stml_user_dashboard/presentation/stml_user_dashboard.dart @@ -1,13 +1,18 @@ // ignore_for_file: avoid_print, prefer_const_constructors // Imported libraries and packages +import 'package:memoryminder/services/notification_service.dart'; import 'package:memoryminder/src/features/caregiver-dashboard/presentation/app_bar.dart'; -import 'package:memoryminder/src/features/help/help_screen.dart'; +import 'package:memoryminder/src/features/caregiver-dashboard/presentation/caregiver-dashboard.dart'; +import 'package:memoryminder/ui/dementia_resources.dart'; +import 'package:memoryminder/ui/help_screen.dart'; import 'package:memoryminder/ui/response_screen.dart'; +import 'package:memoryminder/ui/assistant_screen.dart'; import 'package:memoryminder/src/features/sensitive_information_detection/presentation/audio_screen.dart'; import 'package:memoryminder/ui/gallery_screen.dart'; import 'package:memoryminder/ui/profile_screen.dart'; import 'package:memoryminder/ui/scam_detection_screen.dart'; +import 'package:memoryminder/ui/tour_screen.dart'; import 'package:memoryminder/ui/location_history_screen.dart'; import 'package:geocoding/geocoding.dart'; import 'package:geolocator/geolocator.dart'; @@ -15,15 +20,16 @@ import 'package:memoryminder/src/camera_manager.dart'; import 'package:memoryminder/src/utils/ui_utils.dart'; import 'package:flutter/material.dart'; import 'package:memoryminder/features/caregiver_task_management/caregiver_task_screen.dart'; +import 'package:memoryminder/src/features/wearable-integration/fitbit_login.dart'; // Main HomeScreen widget which is a stateless widget. -class STMLUserDashboardScreen extends StatefulWidget { +class HomeScreen extends StatefulWidget { @override - _STMLUserDashboardScreenState createState() => _STMLUserDashboardScreenState(); + _HomeScreenState createState() => _HomeScreenState(); } -class _STMLUserDashboardScreenState extends State { +class _HomeScreenState extends State { bool hasBeenInitialized = false; double iconSize = 65; @@ -127,13 +133,12 @@ class _STMLUserDashboardScreenState extends State { const Color(0xFF000000).withOpacity(0.30)), _buildElevatedButton( context: context, - icon: Icon(Icons.sos_sharp, - size: iconSize, color: Colors.black54), + icon: Icon(Icons.help_outline, + size: iconSize, color: Colors.white), text: 'HELP', screen: HelpScreen(), keyName: "HelpButtonKey", - backgroundColor: const Color(0xFFFFFFFF).withOpacity(0.30), - ), + backgroundColor: Colors.red.withOpacity(0.80)), _buildElevatedButton( context: context, icon: Icon(Icons.photo, @@ -199,7 +204,6 @@ class _STMLUserDashboardScreenState extends State { bottomNavigationBar: UiUtils.createBottomNavigationBar(context)); } - // Helper function to create each button for the GridView Widget _buildElevatedButton({ required BuildContext context, @@ -224,9 +228,7 @@ class _STMLUserDashboardScreenState extends State { onPressed: () { if (routeName != null) { Navigator.pushNamed(context, routeName); // Use named route if provided - } - else if (screen != null) - { + } else if (screen != null) { Navigator.push( context, MaterialPageRoute(builder: (context) => screen), // Default behavior diff --git a/STML/stml_application/lib/ui/caregiver_alerts_screen.dart b/STML/stml_application/lib/ui/caregiver_alerts_screen.dart new file mode 100644 index 00000000..1ed892b9 --- /dev/null +++ b/STML/stml_application/lib/ui/caregiver_alerts_screen.dart @@ -0,0 +1,268 @@ +// caregiver_alerts_screen.dart +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:intl/intl.dart'; +import 'package:maps_launcher/maps_launcher.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; + +class CaregiverAlertsScreen extends StatefulWidget { + const CaregiverAlertsScreen({Key? key}) : super(key: key); + + @override + State createState() => _CaregiverAlertsScreenState(); +} + +class _CaregiverAlertsScreenState extends State { + bool _isLoading = false; + String? _filterStatus; + + @override + void initState() { + super.initState(); + // Log screen view for analytics + FirebaseAnalytics.instance.logScreenView( + screenName: 'caregiver_alerts_screen', + ); + } + + Future _respondToAlert(String docId, String userId) async { + setState(() { + _isLoading = true; + }); + + try { + // Update status in Firestore + await FirebaseFirestore.instance + .collection('helpRequests') + .doc(docId) + .update({ + 'status': 'responded', + 'responseTime': FieldValue.serverTimestamp(), + }); + + // Log the response for analytics + await FirebaseAnalytics.instance.logEvent( + name: 'emergency_alert_responded', + parameters: { + 'request_id': docId, + 'user_id': userId, + }, + ); + + // Show confirmation + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Response confirmed. The user has been notified.'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Help Requests'), + actions: [ + PopupMenuButton( + onSelected: (value) { + setState(() { + _filterStatus = value == 'all' ? null : value; + }); + }, + itemBuilder: (BuildContext context) => [ + const PopupMenuItem( + value: null, + child: Text('All Requests'), + ), + const PopupMenuItem( + value: 'sent', + child: Text('Pending Requests'), + ), + const PopupMenuItem( + value: 'responded', + child: Text('Responded Requests'), + ), + ], + icon: const Icon(Icons.filter_list), + ), + ], + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : StreamBuilder( + stream: _filterStatus == null + ? FirebaseFirestore.instance + .collection('helpRequests') + .orderBy('timestamp', descending: true) + .snapshots() + : FirebaseFirestore.instance + .collection('helpRequests') + .where('status', isEqualTo: _filterStatus) + .orderBy('timestamp', descending: true) + .snapshots(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (!snapshot.hasData || snapshot.data!.docs.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.notifications_off, + size: 64, color: Colors.grey), + const SizedBox(height: 16), + Text( + _filterStatus == null + ? 'No help requests found' + : 'No $_filterStatus requests found', + style: + const TextStyle(fontSize: 18, color: Colors.grey), + ), + ], + ), + ); + } + + return ListView.builder( + itemCount: snapshot.data!.docs.length, + itemBuilder: (context, index) { + var doc = snapshot.data!.docs[index]; + var data = doc.data() as Map; + var userId = data['userId'] as String? ?? 'unknown'; + var timestamp = data['timestamp'] as Timestamp?; + var location = data['location'] as Map?; + var status = data['status'] as String? ?? 'unknown'; + + return Card( + margin: const EdgeInsets.symmetric( + vertical: 8, horizontal: 16), + elevation: 4, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile( + leading: CircleAvatar( + backgroundColor: status == 'sent' + ? Colors.red + : Colors.green, + child: Icon( + status == 'sent' + ? Icons.warning + : Icons.check, + color: Colors.white, + ), + ), + title: Text( + 'Help requested by ${data['userName'] ?? 'Unknown'}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + subtitle: Text( + timestamp != null + ? DateFormat.yMMMd() + .add_jm() + .format(timestamp.toDate()) + : 'Unknown time', + style: TextStyle( + color: Colors.grey[600], + ), + ), + trailing: status == 'sent' + ? ElevatedButton.icon( + icon: const Icon(Icons.check_circle), + label: const Text('Respond'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + onPressed: () => + _respondToAlert(doc.id, userId), + ) + : Chip( + label: const Text('Responded'), + backgroundColor: Colors.green[100], + labelStyle: + TextStyle(color: Colors.green[800]), + ), + ), + if (location != null && location['address'] != null) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Location:', + style: TextStyle( + fontWeight: FontWeight.bold), + ), + Text(location['address']), + ], + ), + ), + ButtonBar( + alignment: MainAxisAlignment.end, + children: [ + if (location != null && + (location['latitude'] != null && + location['longitude'] != null)) + OutlinedButton.icon( + icon: const Icon(Icons.map), + label: const Text('Open Map'), + onPressed: () { + MapsLauncher.launchCoordinates( + location['latitude'], + location['longitude'], + 'Help request location', + ); + }, + ), + if (location != null && + location['address'] != null) + OutlinedButton.icon( + icon: const Icon(Icons.navigation), + label: const Text('Directions'), + onPressed: () { + MapsLauncher.launchQuery( + location['address']); + }, + ), + ], + ), + ], + ), + ), + ); + }, + ); + }, + ), + ); + } +} diff --git a/STML/stml_application/lib/ui/help_screen.dart b/STML/stml_application/lib/ui/help_screen.dart new file mode 100644 index 00000000..b32ebae3 --- /dev/null +++ b/STML/stml_application/lib/ui/help_screen.dart @@ -0,0 +1,329 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:memoryminder/services/notification_service.dart'; +import 'package:memoryminder/ui/location_history_screen.dart'; +import 'package:memoryminder/src/utils/permission_manager.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'dart:async'; // For StreamSubscription + +class HelpScreen extends StatefulWidget { + const HelpScreen({Key? key}) : super(key: key); + + @override + State createState() => _HelpScreenState(); +} + +class _HelpScreenState extends State { + final NotificationService _notificationService = NotificationService(); + bool _isLoading = false; + bool _helpSent = false; + String? _requestId; + String _statusMessage = ''; + StreamSubscription? _statusSubscription; + + @override + void initState() { + super.initState(); + _notificationService.initialize(); + _checkPermissions(); + + // Log screen view for analytics + FirebaseAnalytics.instance.logScreenView( + screenName: 'help_screen', + ); + } + + @override + void dispose() { + _statusSubscription?.cancel(); + super.dispose(); + } + + Future _checkPermissions() async { + // Check general permissions using the existing PermissionManager + await PermissionManager.requestInitialPermissions(); + + // Additionally check location service + await PermissionManager.checkIfLocationServiceIsActive(context); + + // Also request notification permission (not covered in PermissionManager) + await _requestNotificationPermission(); + } + + Future _requestNotificationPermission() async { + NotificationSettings settings = + await FirebaseMessaging.instance.requestPermission( + alert: true, + badge: true, + sound: true, + provisional: false, + ); + + if (settings.authorizationStatus == AuthorizationStatus.denied) { + if (mounted) { + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Notifications Required"), + content: const Text( + "Notifications are essential for caregiver alerts. Please enable them in your device settings."), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text("OK"), + ), + ], + ), + ); + } + return false; + } + + return settings.authorizationStatus == AuthorizationStatus.authorized; + } + + void _subscribeToHelpRequestUpdates(String requestId) { + _statusSubscription?.cancel(); + + _statusSubscription = FirebaseFirestore.instance + .collection('helpRequests') + .doc(requestId) + .snapshots() + .listen((snapshot) { + if (!mounted) return; + + if (snapshot.exists) { + final status = snapshot.data()?['status']; + + setState(() { + if (status == 'responded') { + _statusMessage = 'Your caregiver has confirmed and is on the way!'; + + // Log status update received + FirebaseAnalytics.instance.logEvent( + name: 'help_request_status_updated', + parameters: { + 'request_id': requestId, + 'status': status, + }, + ); + + // Show a prominent notification + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Your caregiver is on the way!'), + backgroundColor: Colors.green, + duration: Duration(seconds: 10), + ), + ); + } else { + _statusMessage = + 'Help request sent. Waiting for caregiver to respond...'; + } + }); + } + }); + } + + Future _sendHelpRequest() async { + // Check permissions before sending alert + bool hasGeneralPermissions = + await PermissionManager.requestInitialPermissions(); + bool hasLocationService = + await PermissionManager.checkIfLocationServiceIsActive(context); + bool hasNotificationPermission = await _requestNotificationPermission(); + + if (!hasGeneralPermissions || + !hasLocationService || + !hasNotificationPermission) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Required permissions are missing'), + backgroundColor: Colors.orange, + ), + ); + return; + } + + setState(() { + _isLoading = true; + _helpSent = false; + _statusMessage = ''; + }); + + try { + // Get the most recent location entry + final locations = await LocationDatabase.instance.readAllLocations(); + final currentLocation = locations.isNotEmpty ? locations.first : null; + + // Send the help notification with location data + final success = + await _notificationService.sendHelpNotification(currentLocation); + + // Get the request ID from the notification service + _requestId = _notificationService.lastRequestId; + + if (!mounted) return; + + if (success && _requestId != null) { + setState(() { + _helpSent = true; + _statusMessage = + 'Help request sent. Waiting for caregiver to respond...'; + }); + + // Subscribe to updates on this request + _subscribeToHelpRequestUpdates(_requestId!); + + // Show feedback + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Help request sent successfully to caregiver.'), + backgroundColor: Colors.green, + ), + ); + } else { + // Show failure message + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to send help request. Please try again.'), + backgroundColor: Colors.red, + ), + ); + } + } catch (e) { + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + Widget _buildStatusIndicator() { + if (!_helpSent) return const SizedBox.shrink(); + + return Container( + margin: const EdgeInsets.symmetric(vertical: 20, horizontal: 24), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.blue[200]!), + ), + child: Column( + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue[100], + shape: BoxShape.circle, + ), + child: const Icon(Icons.info_outline, color: Colors.blue), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + _statusMessage, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + const LinearProgressIndicator(), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Help'), + ), + body: Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.emergency, + size: 80, + color: Colors.red, + ), + const SizedBox(height: 20), + const Text( + 'Need assistance?', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20), + + // Status indicator (shows when help is sent) + _buildStatusIndicator(), + + const SizedBox(height: 20), + _isLoading + ? const Column( + children: [ + CircularProgressIndicator(color: Colors.red), + SizedBox(height: 10), + Text( + 'Sending emergency alert...', + style: TextStyle(color: Colors.red), + ), + ], + ) + : _helpSent + ? ElevatedButton.icon( + icon: const Icon(Icons.refresh), + label: const Text('SEND ANOTHER ALERT'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + padding: const EdgeInsets.symmetric( + horizontal: 40, vertical: 15), + ), + onPressed: _sendHelpRequest, + ) + : ElevatedButton.icon( + icon: const Icon(Icons.warning_amber_rounded), + label: const Text('SEND EMERGENCY ALERT'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + padding: const EdgeInsets.symmetric( + horizontal: 40, vertical: 15), + ), + onPressed: _sendHelpRequest, + ), + const SizedBox(height: 20), + const Text( + 'This will send your current location to your caregiver', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey), + ), + + if (_helpSent) const SizedBox(height: 40), + ], + ), + ), + ), + ); + } +} diff --git a/STML/stml_application/linux/flutter/generated_plugin_registrant.cc b/STML/stml_application/linux/flutter/generated_plugin_registrant.cc index 3ccd5513..ba1e23e1 100644 --- a/STML/stml_application/linux/flutter/generated_plugin_registrant.cc +++ b/STML/stml_application/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) maps_launcher_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "MapsLauncherPlugin"); + maps_launcher_plugin_register_with_registrar(maps_launcher_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/STML/stml_application/linux/flutter/generated_plugins.cmake b/STML/stml_application/linux/flutter/generated_plugins.cmake index 9ce94c49..d8e70d24 100644 --- a/STML/stml_application/linux/flutter/generated_plugins.cmake +++ b/STML/stml_application/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux flutter_secure_storage_linux + maps_launcher url_launcher_linux ) diff --git a/STML/stml_application/macos/Flutter/GeneratedPluginRegistrant.swift b/STML/stml_application/macos/Flutter/GeneratedPluginRegistrant.swift index 20fb3502..f2302e6c 100644 --- a/STML/stml_application/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/STML/stml_application/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,9 @@ import FlutterMacOS import Foundation import cloud_firestore +import cloud_functions import file_selector_macos +import firebase_analytics import firebase_auth import firebase_core import firebase_messaging @@ -16,6 +18,7 @@ import flutter_tts import flutter_web_auth import geolocator_apple import local_auth_darwin +import maps_launcher import package_info_plus import path_provider_foundation import speech_to_text @@ -26,7 +29,9 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) + FLTFirebaseFunctionsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFunctionsPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) @@ -36,6 +41,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterWebAuthPlugin.register(with: registry.registrar(forPlugin: "FlutterWebAuthPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) + MapsLauncherPlugin.register(with: registry.registrar(forPlugin: "MapsLauncherPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SpeechToTextPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextPlugin")) diff --git a/STML/stml_application/pubspec.yaml b/STML/stml_application/pubspec.yaml index 279902fd..618c50ac 100644 --- a/STML/stml_application/pubspec.yaml +++ b/STML/stml_application/pubspec.yaml @@ -42,7 +42,7 @@ dependencies: timeago: ^3.5.0 path_provider: ^2.1.1 video_thumbnail: ^0.5.3 - flutter_dotenv: ^5.1.0 + flutter_dotenv: ^5.2.1 aws_rekognition_api: ^2.0.0 aws_s3_api: ^2.0.0 camera: ^0.11.1 @@ -66,9 +66,13 @@ dependencies: cloud_firestore: ^5.6.5 flutter_local_notifications: ^18.0.1 fitbitter: ^2.0.4 - flutter_secure_storage: ^8.0.0 + flutter_secure_storage: 8.1.0 http: ^0.13.6 fl_chart: ^0.64.0 + cloud_functions: ^5.3.4 + maps_launcher: ^3.0.0+1 + intl: ^0.19.0 + firebase_analytics: ^11.4.4 dev_dependencies: flutter_test: diff --git a/STML/stml_application/windows/flutter/generated_plugin_registrant.cc b/STML/stml_application/windows/flutter/generated_plugin_registrant.cc index 14cd0546..8e6e6414 100644 --- a/STML/stml_application/windows/flutter/generated_plugin_registrant.cc +++ b/STML/stml_application/windows/flutter/generated_plugin_registrant.cc @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -34,6 +35,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("GeolocatorWindows")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); + MapsLauncherPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MapsLauncherPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/STML/stml_application/windows/flutter/generated_plugins.cmake b/STML/stml_application/windows/flutter/generated_plugins.cmake index 03daf122..348c6bb2 100644 --- a/STML/stml_application/windows/flutter/generated_plugins.cmake +++ b/STML/stml_application/windows/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_tts geolocator_windows local_auth_windows + maps_launcher permission_handler_windows url_launcher_windows ) diff --git a/firebase.json b/firebase.json new file mode 100644 index 00000000..5f923501 --- /dev/null +++ b/firebase.json @@ -0,0 +1,18 @@ +{ + "functions": [ + { + "source": "functions", + "codebase": "default", + "ignore": [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log", + "*.local" + ], + "predeploy": [ + "npm --prefix \"$RESOURCE_DIR\" run lint" + ] + } + ] +} diff --git a/functions/.eslintrc.json b/functions/.eslintrc.json new file mode 100644 index 00000000..b84faaa0 --- /dev/null +++ b/functions/.eslintrc.json @@ -0,0 +1,9 @@ +{ + "root": true, + "env": { + "es6": true, + "node": true + }, + "extends": [], + "rules": {} +} \ No newline at end of file diff --git a/functions/.gitignore b/functions/.gitignore new file mode 100644 index 00000000..21ee8d3d --- /dev/null +++ b/functions/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.local \ No newline at end of file diff --git a/functions/index.js b/functions/index.js new file mode 100644 index 00000000..bd6d04db --- /dev/null +++ b/functions/index.js @@ -0,0 +1,38 @@ +const functions = require("firebase-functions"); +const admin = require("firebase-admin"); +admin.initializeApp(); + +exports.sendHelpAlert = functions.https.onCall(async (data, context) => { + const {caregiverToken, location, userId, userName} = data; + + if (!caregiverToken) { + return {success: false, error: "Token du caregiver manquant"}; + } + + try { + const latitude = location && location.latitude ? location.latitude.toString() : ""; + const longitude = location && location.longitude ? location.longitude.toString() : ""; + const address = location && location.address ? location.address : ""; + + await admin.messaging().send({ + token: caregiverToken, + notification: { + title: "URGENT: Help Needed!", + body: `${userName || "A user"} needs help immediately!` + }, + data: { + type: "help_request", + userId: userId || "", + latitude: latitude, + longitude: longitude, + address: address, + timestamp: new Date().toISOString() + } + }); + + return {success: true}; + } catch (error) { + console.error("Error sending message:", error); + return {success: false, error: error.message}; + } +}); \ No newline at end of file diff --git a/functions/package-lock.json b/functions/package-lock.json new file mode 100644 index 00000000..684b2b87 --- /dev/null +++ b/functions/package-lock.json @@ -0,0 +1,7142 @@ +{ + "name": "functions", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "functions", + "dependencies": { + "firebase-admin": "^12.6.0", + "firebase-functions": "^6.0.1" + }, + "devDependencies": { + "eslint": "^8.15.0", + "eslint-config-google": "^0.14.0", + "firebase-functions-test": "^3.1.0" + }, + "engines": { + "node": "22" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", + "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", + "integrity": "sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==", + "license": "MIT" + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz", + "integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", + "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz", + "integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/component": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.9.tgz", + "integrity": "sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.10.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.8.tgz", + "integrity": "sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.2", + "@firebase/auth-interop-types": "0.2.3", + "@firebase/component": "0.6.9", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.10.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.8.tgz", + "integrity": "sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.9", + "@firebase/database": "1.0.8", + "@firebase/database-types": "1.0.5", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.10.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.5.tgz", + "integrity": "sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.2", + "@firebase/util": "1.10.0" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", + "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.10.0.tgz", + "integrity": "sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.0.tgz", + "integrity": "sha512-88uZ+jLsp1aVMj7gh3EKYH1aulTAMFAp8sH/v5a9w8q8iqSG27RiWLoxSAFr/XocZ9hGiWH1kEnBw+zl3xAgNA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@opentelemetry/api": "^1.3.0", + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.3.3", + "protobufjs": "^7.2.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.1.0.tgz", + "integrity": "sha512-G/FQx5cE/+DqBbOpA5jKsegGwdPniU6PuIEMt+qxWgFxvxuFOzVmp6zYchtYuwAWV5/8Dgs0yAmjvNZv3uXLQg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.15.2", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.15.2.tgz", + "integrity": "sha512-+2k+mcQBb9zkaXMllf2wwR/rI07guAx+eZLWsGTDihW2lJRGfiqB7xu1r7/s4uvSP/T+nAumvzT5TTscwHKJ9A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^4.4.1", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.2.tgz", + "integrity": "sha512-nnR5nmL6lxF8YBqb6gWvEgLdLh/Fn+kvAdX5hUOnt48sNSb0riz/93ASd2E5gvanPA41X6Yp25bIfGRp1SMb2g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.16", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", + "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.13.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", + "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT", + "optional": true + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001707", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", + "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0", + "peer": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.126", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.126.tgz", + "integrity": "sha512-AtH1uLcTC72LA4vfYcEJJkrMk/MY/X0ub8Hv7QGAePW2JkeUFHEL/QfS4J77R6M87Sss8O0OcqReSaN1bpyA+Q==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-google": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", + "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT", + "optional": true + }, + "node_modules/farmhash-modern": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", + "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase-admin": { + "version": "12.7.0", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.7.0.tgz", + "integrity": "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA==", + "license": "Apache-2.0", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "1.0.8", + "@firebase/database-types": "1.0.5", + "@types/node": "^22.0.1", + "farmhash-modern": "^1.1.0", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.1.0", + "node-forge": "^1.3.1", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^7.7.0", + "@google-cloud/storage": "^7.7.0" + } + }, + "node_modules/firebase-functions": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.3.2.tgz", + "integrity": "sha512-FC3A1/nhqt1ZzxRnj5HZLScQaozAcFSD/vSR8khqSoFNOfxuXgwJS6ZABTB7+v+iMD5z6Mmxw6OfqITUBuI7OQ==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "^4.17.21", + "cors": "^2.8.5", + "express": "^4.21.0", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^11.10.0 || ^12.0.0 || ^13.0.0" + } + }, + "node_modules/firebase-functions-test": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-3.4.1.tgz", + "integrity": "sha512-qAq0oszrBGdf4bnCF6t4FoSgMsepeIXh0Pi/FhikSE6e+TvKKGpfrfUP/5pFjJZxFcLsweoau88KydCql4xSeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.14.104", + "lodash": "^4.17.5", + "ts-deepmerge": "^2.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "firebase-functions": ">=4.9.0", + "jest": ">=28.0.0" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/form-data": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", + "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "license": "MIT", + "optional": true + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.4.1.tgz", + "integrity": "sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "optional": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-entities": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.3.tgz", + "integrity": "sha512-D3AfvN7SjhTgBSA8L1BN4FpPzuEd06uy4lHwSoRWr0lndi9BKaNzPLKGOWZ2ocSGguozr08TTb2jhCLHaemruw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz", + "integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==", + "license": "MIT", + "dependencies": { + "@types/express": "^4.17.20", + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^4.15.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT", + "optional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT", + "optional": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "optional": true + }, + "node_modules/ts-deepmerge": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-2.0.7.tgz", + "integrity": "sha512-3phiGcxPSSR47RBubQxPoZ+pqXsEsozLo4G4AlSrsMKTFg9TA3l+3he5BqpUi9wiuDbaHWXH/amlzQ49uEdXtg==", + "dev": true, + "license": "ISC" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT", + "optional": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/functions/package.json b/functions/package.json new file mode 100644 index 00000000..ded79f2b --- /dev/null +++ b/functions/package.json @@ -0,0 +1,27 @@ +{ + "name": "functions", + "description": "Cloud Functions for Firebase", + "scripts": { + "lint": "exit 0", + "build": "echo No build required", + "serve": "firebase emulators:start --only functions", + "shell": "firebase functions:shell", + "start": "npm run shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" +}, + "engines": { + "node": "22" + }, + "main": "index.js", + "dependencies": { + "firebase-admin": "^12.6.0", + "firebase-functions": "^6.0.1" + }, + "devDependencies": { + "eslint": "^8.15.0", + "eslint-config-google": "^0.14.0", + "firebase-functions-test": "^3.1.0" + }, + "private": true +} diff --git a/team_a/.gitignore b/team_a/.gitignore deleted file mode 100644 index 25fd9094..00000000 --- a/team_a/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# See https://www.dartlang.org/guides/libraries/private-files - -# Files and directories created by pub -.dart_tool/ -.packages -build/ -# If you're building an application, you may want to check-in your pubspec.lock -pubspec.lock - -# Directory created by dartdoc -# If you don't generate documentation locally you can remove this line. -doc/api/ - -# dotenv environment variables file -.env* - -# Avoid committing generated Javascript files: -*.dart.js -*.info.json # Produced by the --dump-info flag. -*.js # When generated by dart2js. Don't specify *.js if your - # project includes source files written in JavaScript. -*.js_ -*.js.deps -*.js.map - -.flutter-plugins -.flutter-plugins-dependencies -.vs/* -.vscode/ -.vs* -*/ios/ -linux/ -macos/ -web/ -windows/ -Flutter/ -teama/linux/* -teama/macos/* -teama/windows/* -teama/assets/api-keys.json -teama/android/ diff --git a/team_a/LICENSE b/team_a/LICENSE deleted file mode 100644 index 0e259d42..00000000 --- a/team_a/LICENSE +++ /dev/null @@ -1,121 +0,0 @@ -Creative Commons Legal Code - -CC0 1.0 Universal - - CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE - LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN - ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS - INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES - REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS - PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM - THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED - HEREUNDER. - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator -and subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for -the purpose of contributing to a commons of creative, cultural and -scientific works ("Commons") that the public can reliably and without fear -of later claims of infringement build upon, modify, incorporate in other -works, reuse and redistribute as freely as possible in any form whatsoever -and for any purposes, including without limitation commercial purposes. -These owners may contribute to the Commons to promote the ideal of a free -culture and the further production of creative, cultural and scientific -works, or to gain reputation or greater distribution for their Work in -part through the use and efforts of others. - -For these and/or other purposes and motivations, and without any -expectation of additional consideration or compensation, the person -associating CC0 with a Work (the "Affirmer"), to the extent that he or she -is an owner of Copyright and Related Rights in the Work, voluntarily -elects to apply CC0 to the Work and publicly distribute the Work under its -terms, with knowledge of his or her Copyright and Related Rights in the -Work and the meaning and intended legal effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not -limited to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, - communicate, and translate a Work; - ii. moral rights retained by the original author(s) and/or performer(s); -iii. publicity and privacy rights pertaining to a person's image or - likeness depicted in a Work; - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - v. rights protecting the extraction, dissemination, use and reuse of data - in a Work; - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation - thereof, including any amended or successor version of such - directive); and -vii. other similar, equivalent or corresponding rights throughout the - world based on applicable law or treaty, and any national - implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention -of, applicable law, Affirmer hereby overtly, fully, permanently, -irrevocably and unconditionally waives, abandons, and surrenders all of -Affirmer's Copyright and Related Rights and associated claims and causes -of action, whether now known or unknown (including existing as well as -future claims and causes of action), in the Work (i) in all territories -worldwide, (ii) for the maximum duration provided by applicable law or -treaty (including future time extensions), (iii) in any current or future -medium and for any number of copies, and (iv) for any purpose whatsoever, -including without limitation commercial, advertising or promotional -purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each -member of the public at large and to the detriment of Affirmer's heirs and -successors, fully intending that such Waiver shall not be subject to -revocation, rescission, cancellation, termination, or any other legal or -equitable action to disrupt the quiet enjoyment of the Work by the public -as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason -be judged legally invalid or ineffective under applicable law, then the -Waiver shall be preserved to the maximum extent permitted taking into -account Affirmer's express Statement of Purpose. In addition, to the -extent the Waiver is so judged Affirmer hereby grants to each affected -person a royalty-free, non transferable, non sublicensable, non exclusive, -irrevocable and unconditional license to exercise Affirmer's Copyright and -Related Rights in the Work (i) in all territories worldwide, (ii) for the -maximum duration provided by applicable law or treaty (including future -time extensions), (iii) in any current or future medium and for any number -of copies, and (iv) for any purpose whatsoever, including without -limitation commercial, advertising or promotional purposes (the -"License"). The License shall be deemed effective as of the date CC0 was -applied by Affirmer to the Work. Should any part of the License for any -reason be judged legally invalid or ineffective under applicable law, such -partial invalidity or ineffectiveness shall not invalidate the remainder -of the License, and in such case Affirmer hereby affirms that he or she -will not (i) exercise any of his or her remaining Copyright and Related -Rights in the Work or (ii) assert any associated claims and causes of -action with respect to the Work, in either case contrary to Affirmer's -express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - b. Affirmer offers the Work as-is and makes no representations or - warranties of any kind concerning the Work, express, implied, - statutory or otherwise, including without limitation warranties of - title, merchantability, fitness for a particular purpose, non - infringement, or the absence of latent or other defects, accuracy, or - the present or absence of errors, whether or not discoverable, all to - the greatest extent permissible under applicable law. - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person's Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the - Work. - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to - this CC0 or use of the Work. diff --git a/team_a/MoodlePlugin/.DS_Store b/team_a/MoodlePlugin/.DS_Store deleted file mode 100644 index e9963cd3..00000000 Binary files a/team_a/MoodlePlugin/.DS_Store and /dev/null differ diff --git a/team_a/MoodlePlugin/learninglens/.DS_Store b/team_a/MoodlePlugin/learninglens/.DS_Store deleted file mode 100644 index b69e9e2d..00000000 Binary files a/team_a/MoodlePlugin/learninglens/.DS_Store and /dev/null differ diff --git a/team_a/MoodlePlugin/learninglens/classes/.DS_Store b/team_a/MoodlePlugin/learninglens/classes/.DS_Store deleted file mode 100644 index 6fc52684..00000000 Binary files a/team_a/MoodlePlugin/learninglens/classes/.DS_Store and /dev/null differ diff --git a/team_a/MoodlePlugin/learninglens/classes/external/add_essay_override.php b/team_a/MoodlePlugin/learninglens/classes/external/add_essay_override.php deleted file mode 100644 index 87f6cc25..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/add_essay_override.php +++ /dev/null @@ -1,80 +0,0 @@ -libdir . '/externallib.php'); - -use external_function_parameters; -use external_single_structure; -use external_value; -use external_api; - -class add_essay_override extends external_api { - - /** - * Define parameters for the external function - */ - public static function execute_parameters() { - return new external_function_parameters([ - 'assignid' => new external_value(PARAM_INT, 'ID of the essay'), - 'userid' => new external_value(PARAM_INT, 'User ID (null if group override)', VALUE_DEFAULT, null), - 'groupid' => new external_value(PARAM_INT, 'Group ID (null if user override)', VALUE_DEFAULT, null), - 'allowsubmissionsfromdate' => new external_value(PARAM_INT, 'Time when essay opens (null for default)', VALUE_DEFAULT, null), - 'duedate' => new external_value(PARAM_INT, 'Time when essay closes (null for default)', VALUE_DEFAULT, null), - 'cutoffdate' => new external_value(PARAM_INT, 'Time when essay submission is no longer allowed (null for default)', VALUE_DEFAULT, null), - 'timelimit' => new external_value(PARAM_INT, 'Essay time limit in seconds (null for default)', VALUE_DEFAULT, null), - 'sortorder' => new external_value(PARAM_INT, 'Essay sort order (null for default)', VALUE_DEFAULT, null), - ]); - } - - /** - * Inserts a new quiz override record - */ - public static function execute($assignid, $userid = null, $groupid = null, $allowsubmissionsfromdate = null, $duedate = null, $cutoffdate = null, $timelimit = null, $sortorder = null) { - global $DB; - - // Validate parameters - $params = self::validate_parameters(self::execute_parameters(), [ - 'assignid' => $assignid, - 'userid' => $userid, - 'groupid' => $groupid, - 'allowsubmissionsfromdate' => $allowsubmissionsfromdate, - 'duedate' => $duedate, - 'cutoffdate' => $cutoffdate, - 'timelimit' => $timelimit, - 'sortorder' => $sortorder, - ]); - - // Ensure either a user or group ID is provided - if (empty($params['userid']) && empty($params['groupid'])) { - throw new \moodle_exception('invalidoverride', 'local_learninglens', '', 'Either a userid or groupid must be provided.'); - } - - // Prepare data for insertion - $record = new \stdClass(); - $record->assignid = $params['assignid']; - $record->userid = $params['userid']; - $record->groupid = $params['groupid']; - $record->allowsubmissionsfromdate = $params['allowsubmissionsfromdate']; - $record->duedate = $params['duedate']; - $record->cutoffdate = $params['cutoffdate']; - $record->timelimit = $params['timelimit']; - $record->sortorder = $params['sortorder']; - - // Insert into the database - $overrideid = $DB->insert_record('assign_overrides', $record); - - // Return the inserted override ID - return ['overrideid' => $overrideid]; - } - - /** - * Defines the return structure - */ - public static function execute_returns() { - return new external_single_structure([ - 'overrideid' => new external_value(PARAM_INT, 'ID of the created quiz override'), - ]); - } -} diff --git a/team_a/MoodlePlugin/learninglens/classes/external/add_quiz_override.php b/team_a/MoodlePlugin/learninglens/classes/external/add_quiz_override.php deleted file mode 100644 index 0d9a9116..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/add_quiz_override.php +++ /dev/null @@ -1,80 +0,0 @@ -libdir . '/externallib.php'); - -use external_function_parameters; -use external_single_structure; -use external_value; -use external_api; - -class add_quiz_override extends external_api { - - /** - * Define parameters for the external function - */ - public static function execute_parameters() { - return new external_function_parameters([ - 'quizid' => new external_value(PARAM_INT, 'ID of the quiz'), - 'userid' => new external_value(PARAM_INT, 'User ID (null if group override)', VALUE_DEFAULT, null), - 'groupid' => new external_value(PARAM_INT, 'Group ID (null if user override)', VALUE_DEFAULT, null), - 'timeopen' => new external_value(PARAM_INT, 'Time when quiz opens (null for default)', VALUE_DEFAULT, null), - 'timeclose' => new external_value(PARAM_INT, 'Time when quiz closes (null for default)', VALUE_DEFAULT, null), - 'timelimit' => new external_value(PARAM_INT, 'Quiz time limit in seconds (null for default)', VALUE_DEFAULT, null), - 'attempts' => new external_value(PARAM_INT, 'Allowed attempts (null for default)', VALUE_DEFAULT, null), - 'password' => new external_value(PARAM_TEXT, 'Quiz password (null for default)', VALUE_DEFAULT, null), - ]); - } - - /** - * Inserts a new quiz override record - */ - public static function execute($quizid, $userid = null, $groupid = null, $timeopen = null, $timeclose = null, $timelimit = null, $attempts = null, $password = null) { - global $DB; - - // Validate parameters - $params = self::validate_parameters(self::execute_parameters(), [ - 'quizid' => $quizid, - 'userid' => $userid, - 'groupid' => $groupid, - 'timeopen' => $timeopen, - 'timeclose' => $timeclose, - 'timelimit' => $timelimit, - 'attempts' => $attempts, - 'password' => $password, - ]); - - // Ensure either a user or group ID is provided - if (empty($params['userid']) && empty($params['groupid'])) { - throw new \moodle_exception('invalidoverride', 'local_learninglens', '', 'Either a userid or groupid must be provided.'); - } - - // Prepare data for insertion - $record = new \stdClass(); - $record->quiz = $params['quizid']; - $record->userid = $params['userid']; - $record->groupid = $params['groupid']; - $record->timeopen = $params['timeopen']; - $record->timeclose = $params['timeclose']; - $record->timelimit = $params['timelimit']; - $record->attempts = $params['attempts']; - $record->password = $params['password']; - - // Insert into the database - $overrideid = $DB->insert_record('quiz_overrides', $record); - - // Return the inserted override ID - return ['overrideid' => $overrideid]; - } - - /** - * Defines the return structure - */ - public static function execute_returns() { - return new external_single_structure([ - 'overrideid' => new external_value(PARAM_INT, 'ID of the created quiz override'), - ]); - } -} diff --git a/team_a/MoodlePlugin/learninglens/classes/external/add_type_randoms_to_quiz.php b/team_a/MoodlePlugin/learninglens/classes/external/add_type_randoms_to_quiz.php deleted file mode 100644 index 04ff2598..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/add_type_randoms_to_quiz.php +++ /dev/null @@ -1,65 +0,0 @@ -libdir . '/externallib.php'); -require_once($CFG->dirroot . '/mod/quiz/locallib.php'); - -use context_course; -use core_external\external_api; -use core_external\external_function_parameters; -use core_external\external_multiple_structure; -use core_external\external_single_structure; -use core_external\external_value; -use core_question\local\bank\question_edit_contexts; -use qformat_xml; - -class add_type_randoms_to_quiz extends external_api { - - public static function execute_parameters() { - return new external_function_parameters([ - 'quizid' => new external_value(PARAM_INT, 'The quiz ID'), - 'categoryid' => new external_value(PARAM_INT, 'Question Category ID'), - 'numquestions' => new external_value(PARAM_INT, 'Number of Questions to add') - ]); - } - - public static function execute_returns() { - return new external_value(PARAM_BOOL, 'Returns TRUE if succesful'); - - } - - public static function execute(int $quizid, int $categoryid, int $numquestions): bool { - global $DB; - - $params = self::validate_parameters(self::execute_parameters(), [ - 'quizid' => $quizid, - 'categoryid' => $categoryid, - 'numquestions' => $numquestions, - ]); - - try { - // Fetch the quiz object. - $quiz = $DB->get_record('quiz', ['id' => $quizid], '*', MUST_EXIST); - - // Fetch the module context for the quiz. - $cm = get_coursemodule_from_instance('quiz', $quizid, $quiz->course, false, MUST_EXIST); - $context = \context_module::instance($cm->id); - - quiz_add_random_questions($quiz, $addonpage = 0, $categoryid, $numquestions, false); - - // Recalculate the sumgrades automatically using Moodle's built-in function - quiz_update_sumgrades($quiz); - quiz_grade_item_update($quiz); - - return true; // Return true on successful addition. - - } catch (Exception $e) { - // Log the error message or handle the error as needed. - debugging('Error adding random questions to quiz: ' . $e->getMessage()); - return false; // Return false if an error occurs. - } - } -} diff --git a/team_a/MoodlePlugin/learninglens/classes/external/create_assignment.php b/team_a/MoodlePlugin/learninglens/classes/external/create_assignment.php deleted file mode 100644 index f708f6ac..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/create_assignment.php +++ /dev/null @@ -1,245 +0,0 @@ -libdir . '/externallib.php'); -require_once($CFG->dirroot . '/course/modlib.php'); -require_once($CFG->dirroot . '/mod/assign/lib.php'); -require_once($CFG->dirroot.'/grade/grading/lib.php'); -require_once($CFG->dirroot.'/grade/grading/form/rubric/lib.php'); -require_once($CFG->dirroot.'/course/lib.php'); // For course functions - - -use external_function_parameters; -use external_single_structure; -use external_value; -use context_course; -use context_module; -use coding_exception; -use external_api; - -class create_assignment extends external_api { - - // Define the input parameters for the web service - public static function execute_parameters() { - return new external_function_parameters( - array( - 'courseid' => new external_value(PARAM_INT, 'Course ID'), - 'sectionid' => new external_value(PARAM_INT, 'Section ID'), - 'assignmentName' => new external_value(PARAM_TEXT, 'Assignment name'), - 'startdate' => new external_value(PARAM_TEXT, 'Start date (timestamp)'), - 'enddate' => new external_value(PARAM_TEXT, 'End date (timestamp)'), - 'rubricJson' => new external_value(PARAM_RAW, 'Rubric JSON', VALUE_DEFAULT, ''), - 'description' => new external_value(PARAM_RAW, 'Assignment description', VALUE_DEFAULT, '') - ) - ); - } - - // The actual function that performs the task - public static function execute($courseid, $sectionid, $assignmentName, $startdate, $enddate, $rubricJson = '', $description = '') { - global $USER, $DB; - - // Validate the parameters - $params = self::validate_parameters( - self::execute_parameters(), - array( - 'courseid' => $courseid, - 'sectionid' => $sectionid, - 'assignmentName' => $assignmentName, - 'startdate' => $startdate, - 'enddate' => $enddate, - 'rubricJson' => $rubricJson, - 'description' => $description - ) - ); - - // Convert date strings to timestamps - $startdate_timestamp = strtotime($startdate); - $enddate_timestamp = strtotime($enddate); - - // Validate the conversion - if ($startdate_timestamp === false) { - throw new invalid_parameter_exception('Invalid start date format.'); - } - if ($enddate_timestamp === false) { - throw new invalid_parameter_exception('Invalid end date format.'); - } - - // Capability check - $context = context_course::instance($courseid); - require_capability('mod/assign:addinstance', $context); - - // Prepare the course module data - $moduleid = $DB->get_field('modules', 'id', ['name' => 'assign']); - $cm = new \stdClass(); - $cm->course = $courseid; - $cm->module = $moduleid; - $cm->section = $sectionid; - $cm->visible = 1; - - // Add the course module entry - $cmid = add_course_module($cm); - - // Prepare the assignment data - $assignment_data = new \stdClass(); - $assignment_data->course = $courseid; - $assignment_data->name = $assignmentName; - $assignment_data->intro = $description; - $assignment_data->introformat = FORMAT_HTML; - $assignment_data->duedate = $enddate_timestamp; - $assignment_data->allowsubmissionsfromdate = $startdate_timestamp; - $assignment_data->grade = 100; - $assignment_data->coursemodule = $cmid; - - // Required fields that must be set - $assignment_data->submissiondrafts = 0; // Allow submission drafts: 0 = No, 1 = Yes - $assignment_data->requiresubmissionstatement = 0; // Require submission statement - $assignment_data->sendnotifications = 1; // Send notifications to graders - $assignment_data->sendlatenotifications = 1; // Send notifications about late submissions - $assignment_data->sendstudentnotifications = 1; // Notify students - $assignment_data->teamsubmission = 0; // Team submissions - $assignment_data->requireallteammemberssubmit = 0; // Require all team members to submit - $assignment_data->blindmarking = 0; // Blind marking - $assignment_data->attemptreopenmethod = 'none'; // Attempt reopen method: 'none', 'manual', 'untilpass' - $assignment_data->maxattempts = -1; // Max attempts (-1 for unlimited) - $assignment_data->markingworkflow = 0; // Marking workflow - $assignment_data->markingallocation = 0; // Marking allocation - $assignment_data->cutoffdate = $enddate_timestamp; // Cut-off date - $assignment_data->gradingduedate = 0; // Grading due date - $assignment_data->grade = 100; // Maximum grade - $assignment_data->completionsubmit = 0; // Completion tracking - $assignment_data->alwaysshowdescription = 1; // Always show description - - // Initialize submission plugin settings - $assignment_data->assignsubmission_onlinetext_enabled = 1; // Enable online text submissions - $assignment_data->assignsubmission_file_enabled = 0; // Disable file submissions - - // Create the assignment instance - $assignment_instance = assign_add_instance($assignment_data); - - // Update the course module with the correct instance ID - $DB->set_field('course_modules', 'instance', $assignment_instance, ['id' => $cmid]); - - // Add the course module to the section - course_add_cm_to_section($courseid, $cmid, $sectionid); - - // Set the assignment to use advanced grading (rubric) if rubricJson is provided - if (!empty($rubricJson)) { - // Create the context_module object - $module_context = context_module::instance($cmid); - - require_capability('moodle/grade:managegradingforms', $module_context); - - // Initialize the grading manager - $grading_manager = get_grading_manager($module_context); - $grading_manager->set_area('submissions'); - $grading_manager->set_component('mod_assign'); - $grading_manager->set_active_method('rubric'); - - // Get the controller for the 'rubric' grading method - $rubric_controller = $grading_manager->get_controller('rubric'); - - // Create the rubric definition using the custom function - $rubric_definition = self::create_rubric_definition_from_json($rubricJson); - - $rubric_controller->update_definition($rubric_definition); - $rubricid = $rubric_controller->get_definition()->id; - } - - return array('assignmentid' => $assignment_instance, 'assignmentname' => $assignmentName, 'rubricid' => $rubricid); - } - - public static function execute_returns() { - return new external_single_structure( - array( - 'assignmentid' => new external_value(PARAM_INT, 'Assignment ID'), - 'assignmentname' => new external_value(PARAM_TEXT, 'Assignment name'), - 'rubricid' => new external_value(PARAM_TEXT, 'Rubric ID') - ) - ); - } - - public static function create_rubric_definition_from_json($rubricJson) { - // Decode the JSON input into an associative array - $rubric_data = json_decode($rubricJson, true); - - // Check if the JSON is valid - if (json_last_error() !== JSON_ERROR_NONE) { - throw new coding_exception('Invalid JSON format.'); - } - - // Validate that the required fields (criteria and levels) exist in the JSON - if (!isset($rubric_data['criteria']) || !is_array($rubric_data['criteria'])) { - throw new coding_exception('Invalid rubric format: missing criteria.'); - } - - // Initialize the rubric definition object (stdClass) - $rubric_definition = new \stdClass(); - $rubric_definition->status = 20; // 20 represents 'active' - $rubric_definition->description = ''; // Optional: Add rubric description if needed - $rubric_definition->name = 'Rubric Name'; // Optional: Set the rubric name - - // Initialize the 'rubric' property with 'criteria' and 'options' - $rubric_definition->rubric = array( - 'criteria' => array(), - 'options' => array( - 'sortlevelsasc' => 1, - 'allowscoreoverrides' => 0, - 'showdescriptionteacher' => 1, - 'showdescriptionstudent' => 0, - // Add other options as required - ), - ); - - // Variables to keep track of criterion and level IDs - $criterion_id = 0; - $level_id = 0; - - // Loop through each criterion in the JSON data - foreach ($rubric_data['criteria'] as $criterion_data) { - // Each criterion will be an associative array with 'levels' as an array - $criterion_array = array(); - $criterion_array['id'] = $criterion_id; - $criterion_array['description'] = $criterion_data['description']; - $criterion_array['sortorder'] = $criterion_id; // Sort order starts at 0 - $criterion_array['levels'] = array(); - - // Validate that the levels field exists - if (!isset($criterion_data['levels']) || !is_array($criterion_data['levels'])) { - throw new coding_exception('Invalid rubric format: missing levels for criterion.'); - } - - // Level counter for keys - $level_key = 0; - - // Loop through each level in the criterion - foreach ($criterion_data['levels'] as $level_data) { - // Each level is an associative array with a definition (name) and score (points) - $level_array = array(); - $level_array['id'] = $level_id; // ID is integer - $level_array['definition'] = $level_data['definition']; - $level_array['score'] = $level_data['score']; - - // Assign the level array to the criterion's levels array with 'NEWID' keys - // This is required for the update_definition method to see them as new levels - $criterion_array['levels']['NEWID' . $level_key] = $level_array; - - $level_id++; - $level_key++; - } - - // Assign the criterion array to the rubric_definition's rubric['criteria'] array with 'NEWID' keys - $rubric_definition->rubric['criteria']['NEWID' . $criterion_id] = $criterion_array; - $criterion_id++; - } - - // Add the description_editor property - $rubric_definition->description_editor = array( - 'text' => $rubric_definition->description, - 'format' => FORMAT_HTML, - ); - - return $rubric_definition; - } -} diff --git a/team_a/MoodlePlugin/learninglens/classes/external/create_lesson.php b/team_a/MoodlePlugin/learninglens/classes/external/create_lesson.php deleted file mode 100644 index 1f4628c4..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/create_lesson.php +++ /dev/null @@ -1,245 +0,0 @@ -libdir . '/externallib.php'); -require_once($CFG->dirroot . '/mod/lesson/lib.php'); -require_once($CFG->dirroot . '/course/lib.php'); // For rebuild_course_cache - -use external_function_parameters; -use external_single_structure; -use external_value; -use context_course; -use moodle_exception; -use external_api; -use stdClass; - -/** - * Create a lesson and link it to the course module. - * Allows for advanced configuration (password, showdescription, completion, etc.). - */ -class create_lesson extends external_api { - - public static function execute_parameters(): external_function_parameters { - return new external_function_parameters([ - 'courseid' => new external_value(PARAM_INT, 'Course ID in which to create the lesson'), - 'name' => new external_value(PARAM_TEXT, 'Lesson name/title'), - - 'intro' => new external_value(PARAM_RAW, 'Lesson description/intro', VALUE_OPTIONAL, ''), - 'introformat' => new external_value(PARAM_INT, 'Intro format (1=HTML, 0=MOODLE, etc.)', VALUE_OPTIONAL, FORMAT_HTML), - - // Visibility of intro on course page - 'showdescription' => new external_value( - PARAM_INT, - 'Display description on course page? (1=yes,0=no)', - VALUE_OPTIONAL, - 1 // default to showing description - ), - - // Lesson availability fields - 'available' => new external_value( - PARAM_INT, - 'Timestamp when the lesson opens (defaults to now if 0)', - VALUE_OPTIONAL, - 0 - ), - 'deadline' => new external_value( - PARAM_INT, - 'Timestamp for lesson deadline (0=no deadline)', - VALUE_OPTIONAL, - 0 - ), - 'timelimit' => new external_value( - PARAM_INT, - 'Time limit (seconds). 0 means no limit', - VALUE_OPTIONAL, - 0 - ), - - // Attempts/retakes - 'retake' => new external_value( - PARAM_INT, - 'Allow retakes? (1=yes,0=no)', - VALUE_OPTIONAL, - 1 - ), - 'maxattempts' => new external_value( - PARAM_INT, - 'Maximum number of attempts allowed', - VALUE_OPTIONAL, - 3 - ), - - // Lesson password - 'usepassword' => new external_value( - PARAM_INT, - '1 if a password is required, 0 otherwise', - VALUE_OPTIONAL, - 0 - ), - 'password' => new external_value( - PARAM_RAW, - 'Lesson password if usepassword=1', - VALUE_OPTIONAL, - '' - ), - - // Activity completion in course_modules - 'completion' => new external_value( - PARAM_INT, - 'Enable completion tracking? (1=yes,0=no)', - VALUE_OPTIONAL, - 1 - ), - ]); - } - - public static function execute( - $courseid, - $name, - $intro = '', - $introformat = FORMAT_HTML, - $showdescription = 1, - - $available = 0, - $deadline = 0, - $timelimit = 0, - - $retake = 1, - $maxattempts = 3, - - $usepassword = 0, - $password = '', - - $completion = 1 - ) { - global $DB; - - // 1. Validate parameters - $params = self::validate_parameters( - self::execute_parameters(), - [ - 'courseid' => $courseid, - 'name' => $name, - 'intro' => $intro, - 'introformat' => $introformat, - 'showdescription' => $showdescription, - 'available' => $available, - 'deadline' => $deadline, - 'timelimit' => $timelimit, - 'retake' => $retake, - 'maxattempts' => $maxattempts, - 'usepassword' => $usepassword, - 'password' => $password, - 'completion' => $completion - ] - ); - - // 2. Check if course exists - if (!$DB->record_exists('course', ['id' => $params['courseid']])) { - throw new moodle_exception('invalidcourseid', 'error', '', $params['courseid']); - } - - // 3. Validate context/capability - $context = context_course::instance($params['courseid']); - self::validate_context($context); - require_capability('mod/lesson:addinstance', $context); - - // 4. Create lesson record - $lesson = new stdClass(); - $lesson->course = $params['courseid']; - $lesson->name = $params['name']; - $lesson->intro = $params['intro']; - $lesson->introformat = $params['introformat']; - - // If 'available' is 0, we can default to now => time() - $lesson->available = ($params['available'] == 0) ? time() : $params['available']; - - $lesson->deadline = $params['deadline']; - $lesson->timelimit = $params['timelimit']; - $lesson->retake = $params['retake']; - $lesson->maxattempts = $params['maxattempts']; - - // Lesson password usage - $lesson->usepassword = $params['usepassword']; // field typically in lesson table - $lesson->password = $params['password']; - - $lesson->timecreated = time(); - $lesson->timemodified = time(); - - // If your version of Moodle requires conditions => store empty JSON - $lesson->conditions = '[]'; - - // Insert into mdl_lesson - $lessonid = $DB->insert_record('lesson', $lesson); - if (!$lessonid) { - throw new moodle_exception('errorcreatinglesson', 'local_learninglens'); - } - - // 5. Link lesson to the course modules - // 5a. Get module ID for 'lesson' - $module = $DB->get_record('modules', ['name' => 'lesson'], 'id'); - if (!$module) { - throw new moodle_exception('modulenotfound', 'error'); - } - - // 5b. Find default course section (assuming section=1) - $section = $DB->get_record('course_sections', [ - 'course' => $params['courseid'], - 'section' => 1 - ], 'id,sequence'); - - if (!$section) { - throw new moodle_exception('sectionnotfound', 'error'); - } - - // 5c. Insert a record in mdl_course_modules - $cm = new stdClass(); - $cm->course = $params['courseid']; - $cm->module = $module->id; - $cm->instance = $lessonid; - $cm->section = $section->id; - $cm->added = time(); - $cm->visible = 1; // show the lesson - $cm->visibleold = 1; - $cm->groupmode = 0; - $cm->groupingid = 0; - - // Activity completion - $cm->completion = $params['completion']; - - // "Display description on course page" - $cm->showdescription = $params['showdescription']; - - $courseModuleId = $DB->insert_record('course_modules', $cm); - if (!$courseModuleId) { - throw new moodle_exception('erroraddingmodule', 'local_learninglens'); - } - - // 5d. Update the course section sequence - $sequence = trim($section->sequence . ',' . $courseModuleId, ','); - $section->sequence = $sequence; - $DB->update_record('course_sections', $section); - - // 6. Rebuild cache - rebuild_course_cache($params['courseid'], true); - - // 7. Return lessonId + courseModuleId - return [ - 'lessonId' => $lessonid, - 'courseModuleId' => $courseModuleId, - ]; - } - - public static function execute_returns(): external_single_structure { - return new external_single_structure([ - 'lessonId' => new external_value( - PARAM_INT, 'ID of the newly created lesson' - ), - 'courseModuleId' => new external_value( - PARAM_INT, 'ID of the newly inserted course module record' - ), - ]); - } -} diff --git a/team_a/MoodlePlugin/learninglens/classes/external/create_quiz.php b/team_a/MoodlePlugin/learninglens/classes/external/create_quiz.php deleted file mode 100644 index a601f08f..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/create_quiz.php +++ /dev/null @@ -1,195 +0,0 @@ -libdir . '/externallib.php'); -require_once($CFG->dirroot . '/mod/quiz/lib.php'); -require_once($CFG->dirroot . '/mod/quiz/locallib.php'); -require_once($CFG->libdir . '/gradelib.php'); -require_once($CFG->dirroot . '/question/editlib.php'); -require_once($CFG->dirroot . '/question/engine/lib.php'); -require_once($CFG->dirroot . '/question/lib.php'); -require_once($CFG->dirroot . '/course/lib.php'); -require_once($CFG->dirroot . '/course/modlib.php'); -require_once($CFG->dirroot . '/mod/quiz/report/statistics/report.php'); - -use external_function_parameters; -use external_single_structure; -use external_multiple_structure; -use external_value; -use context_system; -use context_course; -use context_module; -use moodle_exception; -use coding_exception; -use required_capability_exception; -use access_manager; -use external_api; -use qformat_xml; -use question_bank; -use question_edit_contexts; - - -class create_quiz extends \external_api { - -public static function execute_parameters(): external_function_parameters { - return new external_function_parameters( - array( - 'courseid' => new external_value(PARAM_INT, 'ID of the course'), - 'name' => new external_value(PARAM_TEXT, 'Name of the quiz'), - 'intro' => new external_value(PARAM_RAW, 'Introductory text for the quiz'), - 'sectionid' => new external_value(PARAM_INT, 'Section ID', VALUE_DEFAULT, 1), - 'timeopen' => new external_value(PARAM_TEXT, 'Time when the quiz opens', VALUE_DEFAULT, '0'), - 'timeclose' => new external_value(PARAM_TEXT, 'Time when the quiz closes', VALUE_DEFAULT, '0'), - ) - ); -} - -public static function execute_returns(): external_single_structure { - return new external_single_structure( - array( - 'quizid' => new external_value(PARAM_INT, 'ID of the created quiz') - ) - ); -} - -public static function execute($courseid, $name, $intro, $sectionid=1, $timeopen='0', $timeclose='0'): array { - global $DB, $USER; - - // validate params - $params = self::validate_parameters(self::execute_parameters(), array( - 'courseid' => $courseid, - 'name' => $name, - 'intro' => $intro, - 'sectionid' => $sectionid, - 'timeopen' => $timeopen, - 'timeclose' => $timeclose - )); - - // set context - $context = context_course::instance($params['courseid']); - - // ensure the user has permission to create a quiz - self::validate_context($context); - require_capability('mod/quiz:addinstance', $context); - require_capability('mod/quiz:manage', $context); - - // create the course module - $module = new \stdClass(); - $module->course = $params['courseid']; - $module->module = $DB->get_field('modules', 'id', array('name' => 'quiz')); - $module->instance = 0; - $module->section = $params['sectionid']; - $module->visible = 1; - $module->visibleold = 1; - $module->groupmode = 0; - $module->groupingid = 0; - $module->completion = 0; - $module->idnumber = $params['sectionid']; - $module->added = time(); - - // add the course module - $module->coursemodule = add_course_module($module); - if (!$module->coursemodule) { - throw new moodle_exception('Could not create course module'); - } - - // update module ID - $module->id = $module->coursemodule; - - // add course module to section - \course_add_cm_to_section($params['courseid'], $module->id, $params['sectionid']); - - - // Convert date strings to Unix timestamps - $timeopen = strtotime($params['timeopen']); // Convert '12 January 2024' to Unix timestamp - $timeclose = strtotime($params['timeclose']); - - // create the quiz module - $quiz = new \stdClass(); - $quiz->course = $params['courseid']; - $quiz->name = $params['name']; - $quiz->intro = '

' . $params['intro'] . '

'; - $quiz->introformat = FORMAT_HTML; - $quiz->timeopen = $timeopen; - $quiz->timeclose = $timeclose; - $quiz->preferredbehaviour = 'deferredfeedback'; - $quiz->attempts = 0; - $quiz->grade = 100; - $quiz->sumgrades = 1; // this get updated in the add_type_randoms_to_quiz to propertly calculate the sum based on the number of questions. - $quiz->timelimit = 0; - $quiz->overduehandling = 'autosubmit'; - $quiz->graceperiod = 0; - $quiz->timecreated = time(); - $quiz->timemodified = time(); - $quiz->quizpassword = ''; - $quiz->coursemodule = $module->id; - $quiz->feedbackboundarycount = 0; - $quiz->feedbacktext = []; - $quiz->questionsperpage = 1; - $quiz->shuffleanswers = 1; - $quiz->browsersecurity = '-'; - - // process the options from the form. - $result = quiz_process_options($quiz); - if ($result && is_string($result)) { - throw new moodle_exception($result); - } - - // insert the quiz into the database - $quiz->id = $DB->insert_record('quiz', $quiz); - - // update the course module with the quiz instance ID - $DB->set_field('course_modules', 'instance', $quiz->id, array('id' => $module->id)); - - // create the first section for this quiz. - $DB->insert_record('quiz_sections', ['quizid' => $quiz->id, 'firstslot' => 1, 'heading' => '', 'shufflequestions' => 0]); - - // clear feedback - $DB->delete_records('quiz_feedback', ['quizid' => $quiz->id]); - - // set feedback - for ($i = 0; $i <= $quiz->feedbackboundarycount; $i++) { - if (isset($quiz->feedbacktext[$i])) { - $feedback = new \stdClass(); - $feedback->quizid = $quiz->id; - $feedback->feedbacktext = $quiz->feedbacktext[$i]['text'] ?? ''; - $feedback->feedbacktextformat = $quiz->feedbacktext[$i]['format'] ?? FORMAT_HTML; - $feedback->mingrade = $quiz->feedbackboundaries[$i] ?? 0; - $feedback->maxgrade = $quiz->feedbackboundaries[$i - 1] ?? 100; - $feedback->id = $DB->insert_record('quiz_feedback', $feedback); - $feedbacktext = file_save_draft_area_files((int)$quiz->feedbacktext[$i]['itemid'] ?? 0, - $context->id, 'mod_quiz', 'feedback', $feedback->id, - ['subdirs' => false, 'maxfiles' => -1, 'maxbytes' => 0], - $quiz->feedbacktext[$i]['text'] ?? ''); - $DB->set_field('quiz_feedback', 'feedbacktext', $feedbacktext, ['id' => $feedback->id]); - } - } - - // store settings belonging to the access rules - \mod_quiz\access_manager::save_settings($quiz); - - // update events related to this quiz - quiz_update_events($quiz); - $completionexpected = (!empty($quiz->completionexpected)) ? $quiz->completionexpected : null; - \core_completion\api::update_completion_date_event($quiz->coursemodule, 'quiz', $quiz->id, $completionexpected); - - // update related grade item. - quiz_grade_item_update($quiz); - - // update quiz review fields - $quiz->reviewattempt = 69888; - $quiz->reviewcorrectness = 4352; - $quiz->reviewmaxmarks = 69888; - $quiz->reviewmarks = 4352; - $quiz->reviewspecificfeedback = 4352; - $quiz->reviewgeneralfeedback = 4352; - $quiz->reviewrightanswer = 4352; - $quiz->reviewoverallfeedback = 4352; - $DB->update_record('quiz', $quiz); - - // return quiz ID - return array('quizid' => $quiz->id); -} -}; \ No newline at end of file diff --git a/team_a/MoodlePlugin/learninglens/classes/external/delete_lesson_plan.php b/team_a/MoodlePlugin/learninglens/classes/external/delete_lesson_plan.php deleted file mode 100644 index 98a1f965..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/delete_lesson_plan.php +++ /dev/null @@ -1,54 +0,0 @@ -libdir . '/externallib.php'); - -use external_function_parameters; -use external_single_structure; -use external_value; -use external_api; -use context_module; -use context_course; - -class delete_lesson_plan extends external_api { - - public static function execute_parameters() { - return new external_function_parameters([ - 'lessonid' => new external_value(PARAM_INT, 'ID of the lesson plan to delete'), - ]); - } - - public static function execute($lessonid) { - global $DB, $USER; - - $params = self::validate_parameters(self::execute_parameters(), ['lessonid' => $lessonid]); - - // Check if the lesson plan exists - if (!$lesson = $DB->get_record('lesson', ['id' => $lessonid], '*', IGNORE_MISSING)) { - throw new \moodle_exception('invalidlessonid', 'error', '', $lessonid); - } - - // Get course context - $context = context_course::instance($lesson->course); - - // Ensure user has permission to delete lesson plans - require_capability('mod/lesson:manage', $context); - - // Delete the lesson plan - $DB->delete_records('lesson', ['id' => $lessonid]); - - // Clear cache for the lesson plan - purge_all_caches(); - - return ['status' => 'success', 'message' => 'Lesson plan deleted successfully']; - } - - public static function execute_returns() { - return new external_single_structure([ - 'status' => new external_value(PARAM_TEXT, 'Status of the request'), - 'message' => new external_value(PARAM_TEXT, 'Message about the operation result'), - ]); - } -} diff --git a/team_a/MoodlePlugin/learninglens/classes/external/get_all_overrides.php b/team_a/MoodlePlugin/learninglens/classes/external/get_all_overrides.php deleted file mode 100644 index 5fbe0192..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/get_all_overrides.php +++ /dev/null @@ -1,99 +0,0 @@ -libdir . '/externallib.php'); - -use external_function_parameters; -use external_single_structure; -use external_multiple_structure; -use external_value; -use external_api; - -class get_all_overrides extends external_api { - - public static function execute_parameters() { - return new external_function_parameters([]); - } - - public static function execute() { - global $DB; - - $questions = self::get_overrides(); - - return $questions; - } - - private static function get_overrides() { - global $DB; - - $sql = " - SELECT - qo.id AS override_id, - 'quiz' AS assignment_type, - qo.quiz AS assignment_id, - q.name AS assignment_name, - q.course AS course_id, - c.fullname AS course_name, - qo.userid, - CONCAT(u.firstname, ' ', u.lastname) AS fullname, - qo.timeopen AS start_time, - qo.timeclose AS end_time, - qo.timelimit, - NULL AS cutoff_time, - qo.attempts, - qo.password, - NULL AS sortorder - FROM mdl_quiz_overrides qo - LEFT JOIN mdl_quiz q ON qo.quiz = q.id - LEFT JOIN mdl_course c ON q.course = c.id - LEFT JOIN mdl_user u ON qo.userid = u.id - UNION ALL - SELECT - ao.id AS override_id, - 'essay' AS assignment_type, - ao.assignid AS assignment_id, - a.name AS assignment_name, - a.course AS course_id, - c.fullname AS course_name, - ao.userid, - CONCAT(u.firstname, ' ', u.lastname) AS fullname, - ao.allowsubmissionsfromdate AS start_time, - ao.duedate AS end_time, - ao.timelimit, - ao.cutoffdate AS cutoff_time, - NULL AS attempts, - NULL AS password, - ao.sortorder - FROM mdl_assign_overrides ao - LEFT JOIN mdl_assign a ON ao.assignid = a.id - LEFT JOIN mdl_course c ON a.course = c.id - LEFT JOIN mdl_user u ON ao.userid = u.id; - "; - - return $DB->get_records_sql($sql); - } - - public static function execute_returns() { - return new external_multiple_structure( - new external_single_structure([ - 'override_id' => new external_value(PARAM_INT, 'Override ID'), - 'assignment_type' => new external_value(PARAM_TEXT, 'Assignment Type'), - 'assignment_id' => new external_value(PARAM_INT, 'Assignment ID'), - 'assignment_name' => new external_value(PARAM_TEXT, 'Assignment Name'), - 'course_id' => new external_value(PARAM_INT, 'Course ID'), - 'course_name' => new external_value(PARAM_TEXT, 'Course Name'), - 'userid' => new external_value(PARAM_INT, 'User ID'), - 'fullname' => new external_value(PARAM_TEXT, 'Full name'), - 'start_time' => new external_value(PARAM_INT, 'Time Open'), - 'end_time' => new external_value(PARAM_INT, 'Time Close'), - 'timelimit' => new external_value(PARAM_INT, 'Time Limit'), - 'cutoff_time' => new external_value(PARAM_INT, 'Cutoff Time (Essay only)'), - 'attempts' => new external_value(PARAM_INT, 'Attempts (Quiz only)'), - 'password' => new external_value(PARAM_TEXT, 'Password (Quiz only)'), - 'sortorder' => new external_value(PARAM_INT, 'Sortorder (Essay only)'), - ]) - ); - } -} diff --git a/team_a/MoodlePlugin/learninglens/classes/external/get_lesson_plans_by_course.php b/team_a/MoodlePlugin/learninglens/classes/external/get_lesson_plans_by_course.php deleted file mode 100644 index 43f7ce32..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/get_lesson_plans_by_course.php +++ /dev/null @@ -1,65 +0,0 @@ -libdir . '/externallib.php'); - -use external_function_parameters; -use external_single_structure; -use external_multiple_structure; -use external_value; -use external_api; -use context_course; - -class get_lesson_plans_by_course extends external_api { - - public static function execute_parameters() { - return new external_function_parameters([ - 'courseid' => new external_value(PARAM_INT, 'ID of the course'), - ]); - } - - public static function execute($courseid) { - global $DB; - - $params = self::validate_parameters(self::execute_parameters(), ['courseid' => $courseid]); - - // Ensure course exists - if (!$DB->record_exists('course', ['id' => $courseid])) { - throw new \moodle_exception('invalidcourseid', 'error', '', $courseid); - } - - // Ensure user has the correct capability to view lessons - $context = context_course::instance($courseid); - require_capability('mod/lesson:view', $context); - - // Fetch lesson plans - $lessonplans = self::get_lessons_by_course($courseid); - - return array_values($lessonplans); - } - - private static function get_lessons_by_course($courseid) { - global $DB; - - $sql = " - SELECT id, name, intro, timemodified - FROM {lesson} - WHERE course = :courseid - "; - - return $DB->get_records_sql($sql, ['courseid' => $courseid]); - } - - public static function execute_returns() { - return new external_multiple_structure( - new external_single_structure([ - 'id' => new external_value(PARAM_INT, 'Lesson Plan ID'), - 'name' => new external_value(PARAM_TEXT, 'Lesson Plan Name'), - 'intro' => new external_value(PARAM_RAW, 'Lesson Plan Description'), - 'timemodified' => new external_value(PARAM_INT, 'Last Modified Timestamp'), - ]) - ); - } -} diff --git a/team_a/MoodlePlugin/learninglens/classes/external/get_question_stats_from_quiz.php b/team_a/MoodlePlugin/learninglens/classes/external/get_question_stats_from_quiz.php deleted file mode 100644 index 7ee2df98..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/get_question_stats_from_quiz.php +++ /dev/null @@ -1,156 +0,0 @@ -libdir . '/externallib.php'); - -use external_function_parameters; -use external_single_structure; -use external_multiple_structure; -use external_value; -use external_api; - -class get_question_stats_from_quiz extends external_api { - - /** - * Define parameter structure for the function. - */ - public static function execute_parameters() { - return new external_function_parameters([ - 'quizid' => new external_value(PARAM_INT, 'ID of the quiz'), - ]); - } - - /** - * Main execution function. This is what gets called by the web service. - */ - public static function execute($quizid) { - // Validate incoming parameters. - $params = self::validate_parameters( - self::execute_parameters(), - ['quizid' => $quizid] - ); - - // *Optional*: permission checks (capabilities, context, etc.) - - // Actually fetch data. - $records = self::get_question_stats_by_quiz($params['quizid']); - - // Return the array of data. - return $records; - } - - /** - * The real logic. Query for questions (similar to your existing logic) - * but also compute correct/incorrect stats or any other analytics. - */ - private static function get_question_stats_by_quiz($quizid) { - global $DB; - - // 1) Gather the questions (same as before) - $sql_questions = " - SELECT q.id, - q.name, - q.questiontext, - q.qtype - FROM {question} q - WHERE q.id IN ( - SELECT qbe.id - FROM {question_bank_entries} qbe - WHERE qbe.questioncategoryid = ( - SELECT qc.id - FROM {question_categories} qc - WHERE ( - SELECT quiz.intro - FROM {quiz} quiz - WHERE quiz.id = :quizid - ) LIKE CONCAT('%', qc.name, '%') - LIMIT 1 - ) - ) - "; - - $questions = $DB->get_records_sql($sql_questions, ['quizid' => $quizid]); - if (!$questions) { - return []; - } - - // 2) Gather attempt stats by looking for 'gradedright', 'gradedwrong', 'gradedpartial' - // instead of 'complete' or fraction-based checks - $sql_stats = " - SELECT - qa.questionid, - SUM(CASE WHEN qs.state = 'gradedright' THEN 1 ELSE 0 END) AS numcorrect, - SUM(CASE WHEN qs.state = 'gradedwrong' THEN 1 ELSE 0 END) AS numincorrect, - SUM(CASE WHEN qs.state = 'gradedpartial' THEN 1 ELSE 0 END) AS numpartial, - COUNT(*) AS totalattempts - FROM {quiz_attempts} quiza - JOIN {question_usages} qu ON qu.id = quiza.uniqueid - JOIN {question_attempts} qa ON qa.questionusageid = qu.id - JOIN {question_attempt_steps} qs ON qs.questionattemptid = qa.id - AND qs.sequencenumber = ( - SELECT MAX(qs2.sequencenumber) - FROM {question_attempt_steps} qs2 - WHERE qs2.questionattemptid = qa.id - ) - WHERE quiza.quiz = :quizid2 - GROUP BY qa.questionid - "; - - $statsrecords = $DB->get_records_sql($sql_stats, ['quizid2' => $quizid]); - - // 3) Merge stats with the question array - $results = []; - foreach ($questions as $q) { - $qid = $q->id; - - // Default zeros - $correct = 0; - $incorrect = 0; - $partial = 0; - $total = 0; - - if (isset($statsrecords[$qid])) { - $record = $statsrecords[$qid]; - $correct = (int) $record->numcorrect; - $incorrect = (int) $record->numincorrect; - $partial = (int) $record->numpartial; - $total = (int) $record->totalattempts; - } - - $results[] = [ - 'id' => $qid, - 'name' => $q->name, - 'questiontext' => $q->questiontext, - 'qtype' => $q->qtype, - 'numcorrect' => $correct, - 'numincorrect' => $incorrect, - 'numpartial' => $partial, - 'totalattempts'=> $total, - ]; - } - - return $results; - } - - - /** - * Define the return structure. - */ - public static function execute_returns() { - return new external_multiple_structure( - new external_single_structure([ - 'id' => new external_value(PARAM_INT, 'Question ID'), - 'name' => new external_value(PARAM_TEXT, 'Question name'), - 'questiontext' => new external_value(PARAM_RAW, 'Question text'), - 'qtype' => new external_value(PARAM_ALPHANUMEXT, 'Question type'), - 'numcorrect' => new external_value(PARAM_INT, 'Number of fully correct attempts'), - 'numincorrect' => new external_value(PARAM_INT, 'Number of fully incorrect attempts'), - 'numpartial' => new external_value(PARAM_INT, 'Number of partially correct attempts'), - 'totalattempts' => new external_value(PARAM_INT, 'Total attempts (final steps)'), - ]) - ); - } - -} diff --git a/team_a/MoodlePlugin/learninglens/classes/external/get_questions_from_quiz.php b/team_a/MoodlePlugin/learninglens/classes/external/get_questions_from_quiz.php deleted file mode 100644 index b884c1e0..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/get_questions_from_quiz.php +++ /dev/null @@ -1,66 +0,0 @@ -libdir . '/externallib.php'); - -use external_function_parameters; -use external_single_structure; -use external_multiple_structure; -use external_value; -use external_api; - -class get_questions_from_quiz extends external_api { - - public static function execute_parameters() { - return new external_function_parameters([ - 'quizid' => new external_value(PARAM_INT, 'ID of the quiz'), - ]); - } - - public static function execute($quizid) { - global $DB; - - $params = self::validate_parameters(self::execute_parameters(), ['quizid' => $quizid]); - - $questions = self::get_questions_by_quiz($quizid); - - return $questions; - } - - private static function get_questions_by_quiz($quizid) { - global $DB; - - $sql = " - SELECT q.id, q.name, q.questiontext, q.qtype - FROM {question} q - WHERE q.id IN ( - SELECT qbe.id - FROM {question_bank_entries} qbe - WHERE qbe.questioncategoryid = ( - SELECT qc.id - FROM {question_categories} qc - WHERE ( - SELECT quiz.intro - FROM {quiz} quiz - WHERE quiz.id = :quizid - ) LIKE CONCAT('%', qc.name, '%') - LIMIT 1 - ) - )"; - - return $DB->get_records_sql($sql, ['quizid' => $quizid]); - } - - public static function execute_returns() { - return new external_multiple_structure( - new external_single_structure([ - 'id' => new external_value(PARAM_INT, 'Question ID'), - 'name' => new external_value(PARAM_TEXT, 'Question name'), - 'questiontext' => new external_value(PARAM_RAW, 'Question text'), - 'qtype' => new external_value(PARAM_ALPHANUMEXT, 'Question type'), - ]) - ); - } -} diff --git a/team_a/MoodlePlugin/learninglens/classes/external/get_rubric.php b/team_a/MoodlePlugin/learninglens/classes/external/get_rubric.php deleted file mode 100644 index 256bfb4c..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/get_rubric.php +++ /dev/null @@ -1,102 +0,0 @@ -libdir . '/externallib.php'); -require_once("$CFG->dirroot/grade/grading/lib.php"); -require_once("$CFG->dirroot/mod/assign/locallib.php"); - -use external_function_parameters; -use external_single_structure; -use external_multiple_structure; -use external_value; -use context_module; -use external_api; - -class get_rubric extends external_api { - - public static function execute_parameters() { - return new external_function_parameters([ - 'assignmentid' => new external_value(PARAM_INT, 'ID of the assignment'), - ]); - } - - public static function execute($assignmentid) { - global $USER; - - // Validate the parameters. - $params = self::validate_parameters(self::execute_parameters(), [ - 'assignmentid' => $assignmentid, - ]); - - // Fetch the definition ID for the given assignment. - // $definitionid = self::get_definitionid_from_assignmentid($assignmentid); - $rubricdata[] = self::get_definitionid_from_assignmentid($assignmentid); - return $rubricdata; - } - - public static function get_definitionid_from_assignmentid($assignmentid) { - global $DB; - - //Get the course module ID (`cmid`) for the assignment. - $cmid = $DB->get_field_sql(" - SELECT cm.id - FROM {course_modules} cm - JOIN {modules} m ON m.id = cm.module - JOIN {assign} a ON a.id = cm.instance - WHERE m.name = 'assign' AND a.id = ?", [$assignmentid]); - - if (!$cmid) { - throw new \moodle_exception('invalidassignmentid', 'error', '', $assignmentid); - } - - //Fetch the context of the assignment using the `cmid`. - $context = context_module::instance($cmid); - - //Identify the component. For assignments, it is usually 'mod_assign'. - $component = 'mod_assign'; - - //Initialize the grading manager without context first. - $gradingmanager = new \grading_manager(); - $gradingmanager->set_context($context); - $gradingmanager->set_component($component); - - //Get available grading areas for the specified component and context. - $areas = $gradingmanager->get_available_areas(); - if (!in_array('submissions', array_keys($areas))) { - return null; // 'submissions' grading area not found. - } - $gradingmanager->set_area('submissions'); - - //Get the controller for the 'submissions' grading area. - $controller = $gradingmanager->get_controller('rubric'); - - //Check if the grading method is 'rubric' and get the definition ID. - if (!is_null($controller)) { - return $controller->get_definition(); - } - return null; - } - - public static function execute_returns() { - return new external_multiple_structure( - new external_single_structure([ - 'id' => new external_value(PARAM_INT, 'Instance ID'), - 'rubric_criteria' => new external_multiple_structure( - new external_single_structure([ - 'id' => new external_value(PARAM_INT, 'Criterion ID'), - 'description' => new external_value(PARAM_TEXT, 'Criterion description'), - 'levels' => new external_multiple_structure( - new external_single_structure([ - 'id' => new external_value(PARAM_INT, 'Level ID'), - 'score' => new external_value(PARAM_FLOAT, 'Level score'), - 'definition' => new external_value(PARAM_TEXT, 'Level definition'), - ]) - ), - ]) - ), - ]) - ); - } -} diff --git a/team_a/MoodlePlugin/learninglens/classes/external/get_rubric_grades.php b/team_a/MoodlePlugin/learninglens/classes/external/get_rubric_grades.php deleted file mode 100644 index 5c50f84e..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/get_rubric_grades.php +++ /dev/null @@ -1,107 +0,0 @@ -libdir . '/externallib.php'); - require_once("$CFG->dirroot/grade/grading/lib.php"); - require_once("$CFG->dirroot/mod/assign/locallib.php"); - - use external_function_parameters; - use external_single_structure; - use external_multiple_structure; - use external_value; - use context_module; - use external_api; - -class get_rubric_grades extends external_api { - - public static function execute_parameters() { - return new external_function_parameters([ - 'assignmentid' => new external_value(PARAM_INT, 'ID of the assignment'), - 'userid' => new external_value(PARAM_INT, 'ID of the user') - ]); - } - - // Define the return structure for the function. - public static function execute_returns() { - return new external_multiple_structure( - new external_single_structure( - array( - 'criterionid' => new external_value(PARAM_INT, 'The ID of the criterion'), - 'criterion_description' => new external_value(PARAM_TEXT, 'The description of the criterion'), - 'levelid' => new external_value(PARAM_INT, 'The ID of the level'), - 'level_description' => new external_value(PARAM_TEXT, 'The description of the level in the criterion'), - 'score' => new external_value(PARAM_FLOAT, 'The score for the criterion'), - 'remark' => new external_value(PARAM_RAW, 'Remarks by the grader') - ) - ) - ); - } - public static function execute($assignmentid, $userid) { - global $USER; - - // Validate the parameters. - $params = self::validate_parameters(self::execute_parameters(), [ - 'assignmentid' => $assignmentid, - 'userid' => $userid - ]); - - global $DB; - - // Get the assignment's grading instance ID. - $itemid = $DB->get_field('assign_grades', 'id', ['assignment' => $assignmentid, 'userid' => $userid], MUST_EXIST); - - // Add conditions to ensure you get only one grading instance. - $gradinginstances = $DB->get_records('grading_instances', ['itemid' => $itemid]); - - - $valid_gradinginstance = null; - // Loop through grading instances to find the one with status = 1 and method = 'rubric'. - foreach ($gradinginstances as $instance) { - if ($instance->status == 1) { - // Get the definition for the grading instance. - $definition = $DB->get_record('grading_definitions', ['id' => $instance->definitionid], '*', IGNORE_MISSING); - if ($definition && $definition->method === 'rubric') { - $valid_gradinginstance = $instance; - break; // Stop once we find the valid instance. - } - } - } - - if (!$valid_gradinginstance) { - throw new moodle_exception('norubricinstance', 'error', '', $assignmentid); - } - - // Query the rubric fillings for this submission. - $rubricgrades = $DB->get_records('gradingform_rubric_fillings', ['instanceid' => $valid_gradinginstance->id]); - - $rubricdata = []; - - foreach ($rubricgrades as $grade) { - $criterion = $DB->get_record('gradingform_rubric_criteria', ['id' => $grade->criterionid], '*', MUST_EXIST); - $level = $DB->get_record('gradingform_rubric_levels', ['id' => $grade->levelid], '*', MUST_EXIST); - $rubricdata[] = [ - 'criterion_description' => $criterion->description, - 'level_description' => $level->definition, - 'score' => $level->score, - 'remark' => $grade->remark, - 'criterionid' => $criterion->id, - 'levelid' => $level->id - ]; - } - - return $rubricdata; - - } -} \ No newline at end of file diff --git a/team_a/MoodlePlugin/learninglens/classes/external/import_questions.php b/team_a/MoodlePlugin/learninglens/classes/external/import_questions.php deleted file mode 100644 index 13e9455b..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/import_questions.php +++ /dev/null @@ -1,92 +0,0 @@ - new external_value(PARAM_INT, 'The course ID'), - 'questionxml' => new external_value(PARAM_RAW, 'Question XML'), - ]); - } - - /** - * Defines function return values - * @return external_single_structure - */ - public static function execute_returns(): external_single_structure { - return new external_single_structure( - array( - 'categoryid' => new external_value(PARAM_INT, 'ID of the created category') - ) - ); - } - - /** - * Execute the function. - * @param $courseid - * @param $questionxml - * @return array - * @throws \invalid_parameter_exception - * @throws \moodle_exception - */ - public static function execute($courseid, $questionxml) { - global $CFG; - require_once("$CFG->dirroot/lib/questionlib.php"); - require_once("$CFG->dirroot/lib/datalib.php"); - require_once("$CFG->dirroot/question/format/xml/format.php"); - - $params = self::validate_parameters(self::execute_parameters(), [ - 'courseid' => $courseid, - 'questionxml' => $questionxml, - ]); - - // write XML data to temp file - $tmpfile = tmpfile(); - fwrite($tmpfile, $questionxml); - $metadata = stream_get_meta_data($tmpfile); - $tmpfilename = $metadata['uri']; - - // Set up the import formatter - $qformat = new qformat_xml(); - $qformat->setContexts((new question_edit_contexts(context_course::instance($courseid)))->all()); - $qformat->setCourse(get_course($courseid)); - $qformat->setFilename($tmpfilename); - $qformat->setMatchgrades('error'); - $qformat->setCatfromfile(1); - $qformat->setContextfromfile(1); - $qformat->setStoponerror(1); - $qformat->setCattofile(1); - $qformat->setContexttofile(1); - $qformat->set_display_progress(false); - - // Do the import - $imported = $qformat->importprocess(); - - // // Return list of question IDs - // $retval = array(); - // for ($i = 0; $i < count($qformat->questionids); $i++) { - // $retval[$i] = ['questionid' => $qformat->questionids[$i]]; - // } - // return $retval; - - // Return Category ID - return array('categoryid' => $qformat->category->id); - - } -} \ No newline at end of file diff --git a/team_a/MoodlePlugin/learninglens/classes/external/update_lesson_plan.php b/team_a/MoodlePlugin/learninglens/classes/external/update_lesson_plan.php deleted file mode 100644 index b33f8087..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/update_lesson_plan.php +++ /dev/null @@ -1,76 +0,0 @@ -libdir . '/externallib.php'); - -use external_function_parameters; -use external_single_structure; -use external_value; -use external_api; -use context_course; - -class update_lesson_plan extends external_api { - - public static function execute_parameters() { - return new external_function_parameters([ - 'lessonid' => new external_value(PARAM_INT, 'ID of the lesson plan to update'), - 'name' => new external_value(PARAM_TEXT, 'Updated lesson plan name', VALUE_OPTIONAL), - 'intro' => new external_value(PARAM_RAW, 'Updated lesson plan description', VALUE_OPTIONAL), - 'available' => new external_value(PARAM_INT, 'Updated available timestamp', VALUE_OPTIONAL), - 'deadline' => new external_value(PARAM_INT, 'Updated deadline timestamp', VALUE_OPTIONAL), - ]); - } - - public static function execute($lessonid, $name = null, $intro = null, $available = null, $deadline = null) { - global $DB, $USER; - - $params = self::validate_parameters(self::execute_parameters(), [ - 'lessonid' => $lessonid, - 'name' => $name, - 'intro' => $intro, - 'available' => $available, - 'deadline' => $deadline - ]); - - // Check if the lesson plan exists - if (!$lesson = $DB->get_record('lesson', ['id' => $lessonid], '*', IGNORE_MISSING)) { - throw new \moodle_exception('invalidlessonid', 'error', '', $lessonid); - } - - // Get course context - $context = context_course::instance($lesson->course); - - // Ensure user has permission to update lesson plans - require_capability('mod/lesson:manage', $context); - - // Prepare update data - $updateData = ['id' => $lessonid]; - - if (!is_null($name)) { - $updateData['name'] = $name; - } - if (!is_null($intro)) { - $updateData['intro'] = $intro; - } - if (!is_null($available)) { - $updateData['available'] = $available; - } - if (!is_null($deadline)) { - $updateData['deadline'] = $deadline; - } - - // Perform update - $DB->update_record('lesson', (object)$updateData); - - return ['status' => 'success', 'message' => 'Lesson plan updated successfully']; - } - - public static function execute_returns() { - return new external_single_structure([ - 'status' => new external_value(PARAM_TEXT, 'Status of the request'), - 'message' => new external_value(PARAM_TEXT, 'Message about the operation result'), - ]); - } -} diff --git a/team_a/MoodlePlugin/learninglens/classes/external/write_grades.php b/team_a/MoodlePlugin/learninglens/classes/external/write_grades.php deleted file mode 100644 index 38e39737..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/write_grades.php +++ /dev/null @@ -1,110 +0,0 @@ -libdir . '/externallib.php'); -require_once("$CFG->dirroot/grade/grading/lib.php"); -require_once("$CFG->dirroot/mod/assign/locallib.php"); - -use external_function_parameters; -use external_single_structure; -use external_value; -use context_module; -use external_api; - -class write_grades extends external_api { - - public static function execute_parameters() { - return new external_function_parameters([ - 'assignmentid' => new external_value(PARAM_INT, 'ID of the assignment'), - 'userid' => new external_value(PARAM_INT, 'ID of the user'), - 'grade' => new external_value(PARAM_INT, 'Raw grade value') - ]); - } - - // Define the return structure for the function. - public static function execute_returns() { - return new external_value(PARAM_BOOL, 'True if the grades were successfully written.'); - } - - public static function execute($assignmentid, $userid, $grade, $comment = '') { - global $DB, $USER; - - // Validate the parameters. - $params = self::validate_parameters(self::execute_parameters(), [ - 'assignmentid' => $assignmentid, - 'userid' => $userid, - 'grade' => $grade - ]); - - - // Check if there is already a record in the assign_grades table for this user and assignment. - $grade_record = $DB->get_record('assign_grades', ['assignment' => $assignmentid, 'userid' => $userid]); - - // If no grade record exists, insert a new one. - if (!$grade_record) { - $new_grade = new \stdClass(); - $new_grade->assignment = $assignmentid; - $new_grade->userid = $userid; - $new_grade->grader = $USER->id; // Set the current user as the grader. - $new_grade->timecreated = time(); - $new_grade->timemodified = time(); - $new_grade->grade = -1; // Initial placeholder grade (to be updated later). - $new_grade->attemptnumber = 0; // Assuming this is the first attempt. - $new_grade->id = $DB->insert_record('assign_grades', $new_grade); - $itemid = $new_grade->id; // Use the newly inserted record's ID as the itemid. - } else { - // Use the existing grade record's ID as the itemid. - $itemid = $grade_record->id; - } - - // Get the course module for the assignment. - $course_module_id = $DB->get_field('course_modules', 'id', [ - 'instance' => $assignmentid, - 'module' => $DB->get_field('modules', 'id', ['name' => 'assign']) - ]); - - if (!$course_module_id) { - throw new \moodle_exception('nocoursemodule', 'error', '', 'Course module not found for assignment.'); - } - - // Get the assignment's grading rubric controller. - $context = context_module::instance($course_module_id); - $gradingmanager = get_grading_manager($context, 'mod_assign', 'submissions'); - - - - // Update the grade in the assign_grades table to reflect the final state. - $finalgrade = $DB->get_record('assign_grades', ['id' => $itemid], '*', MUST_EXIST); - $finalgrade->grader = $USER->id; - $finalgrade->grade = $grade; - $finalgrade->timemodified = time(); - $DB->update_record('assign_grades', $finalgrade); - - - $gradeupdate = new \stdClass(); - $gradeupdate->id = $finalgrade->id; - $gradeupdate->assignment = $assignmentid; - $gradeupdate->userid = $userid; - $gradeupdate->grade = $grade; - $gradeupdate->rawgrade = $grade; - $gradeupdate->grader = $USER->id; - $gradeupdate->timecreated = $finalgrade->timecreated; - $gradeupdate->timemodified = time(); - $assign = new \assign($context, null, null); - $assign->update_grade($gradeupdate); - - return true; - } - -} diff --git a/team_a/MoodlePlugin/learninglens/classes/external/write_rubric_grades.php b/team_a/MoodlePlugin/learninglens/classes/external/write_rubric_grades.php deleted file mode 100644 index 856a8120..00000000 --- a/team_a/MoodlePlugin/learninglens/classes/external/write_rubric_grades.php +++ /dev/null @@ -1,199 +0,0 @@ -libdir . '/externallib.php'); -require_once("$CFG->dirroot/grade/grading/lib.php"); -require_once("$CFG->dirroot/mod/assign/locallib.php"); -require_once($CFG->dirroot . '/grade/grading/form/rubric/lib.php'); - -use external_function_parameters; -use external_single_structure; -use external_value; -use context_module; -use external_api; - -class write_rubric_grades extends external_api { - - public static function execute_parameters() { - return new external_function_parameters([ - 'assignmentid' => new external_value(PARAM_INT, 'ID of the assignment'), - 'userid' => new external_value(PARAM_INT, 'ID of the user'), - 'rubricgrades' => new external_value(PARAM_RAW, 'Rubric grading data in JSON string format') - ]); - } - - // Define the return structure for the function. - public static function execute_returns() { - return new external_value(PARAM_BOOL, 'True if the rubric grades were successfully written.'); - } - - public static function execute($assignmentid, $userid, $rubricgrades, $comment = '') { - global $DB, $USER; - - // Validate the parameters. - $params = self::validate_parameters(self::execute_parameters(), [ - 'assignmentid' => $assignmentid, - 'userid' => $userid, - 'rubricgrades' => $rubricgrades - ]); - - // Decode the JSON string into an array. - $rubricgrades_array = json_decode($rubricgrades, true); - - // Check if the JSON decoding was successful. - if (json_last_error() !== JSON_ERROR_NONE) { - throw new \moodle_exception('invalidjson', 'error', '', json_last_error_msg()); - } - - // Check if there is already a record in the assign_grades table for this user and assignment. - $grade_record = $DB->get_record('assign_grades', ['assignment' => $assignmentid, 'userid' => $userid]); - - // If no grade record exists, insert a new one. - if (!$grade_record) { - $new_grade = new \stdClass(); - $new_grade->assignment = $assignmentid; - $new_grade->userid = $userid; - $new_grade->grader = $USER->id; // Set the current user as the grader. - $new_grade->timecreated = time(); - $new_grade->timemodified = time(); - $new_grade->grade = -1; // Initial placeholder grade (to be updated later). - $new_grade->attemptnumber = 0; // Assuming this is the first attempt. - $new_grade->id = $DB->insert_record('assign_grades', $new_grade); - $itemid = $new_grade->id; // Use the newly inserted record's ID as the itemid. - } else { - // Use the existing grade record's ID as the itemid. - $itemid = $grade_record->id; - } - - // Get the course module for the assignment. - $course_module_id = $DB->get_field('course_modules', 'id', [ - 'instance' => $assignmentid, - 'module' => $DB->get_field('modules', 'id', ['name' => 'assign']) - ]); - - if (!$course_module_id) { - throw new \moodle_exception('nocoursemodule', 'error', '', 'Course module not found for assignment.'); - } - - // Get the assignment's grading rubric controller. - $context = context_module::instance($course_module_id); - $gradingmanager = get_grading_manager($context, 'mod_assign', 'submissions'); - - // Check if the assignment is using the rubric grading method. - if (!$gradingmanager->get_active_method() || $gradingmanager->get_active_method() !== 'rubric') { - throw new \moodle_exception('norubriccontroller', 'error', '', 'This assignment is not using the rubric grading method.'); - } - - $controller = $gradingmanager->get_controller('rubric'); - - // Check if there's already an existing grading instance for the rater and item. - $gradinginstance = $controller->get_current_instance($USER->id, $itemid); - - - if (!$gradinginstance) { - // If no instance exists, create a new one. - $gradinginstance = $controller->create_instance($USER->id, $itemid); - } - - // Get the instance ID for further operations. - $instanceid = $gradinginstance->get_data('id'); - - // Initialize variables to calculate the total score and maximum score. - $total_score = 0; - $max_score = 0; - - // Get the rubric definition from the grading controller, which contains the criteria and levels. - $rubricdefinition = $controller->get_definition(); - // Now get the criteria from the rubric definition. - $rubriccriteria = $DB->get_records('gradingform_rubric_criteria', ['definitionid' => $rubricdefinition->id]); - - - // Calculate the maximum possible score based on the full rubric definition. - foreach ($rubriccriteria as $criterion) { - $maxlevel = $DB->get_record_sql( - 'SELECT MAX(score) as maxscore FROM {gradingform_rubric_levels} WHERE criterionid = ?', - [$criterion->id] - ); - $max_score += $maxlevel->maxscore; - } - - // Write rubric grades for each criterion and calculate the total score for the graded criteria. - foreach ($rubricgrades_array as $grade) { - // Ensure the criterion and level exist. - $criterion = $DB->get_record('gradingform_rubric_criteria', ['id' => $grade['criterionid']], '*', MUST_EXIST); - $level = $DB->get_record('gradingform_rubric_levels', ['id' => $grade['levelid'], 'criterionid' => $criterion->id], '*', MUST_EXIST); - - // Add the selected level score to the total score. - $total_score += $level->score; - - // Check if there's already an entry for this instance and criterion, update or insert as necessary. - $existinggrade = $DB->get_record('gradingform_rubric_fillings', ['instanceid' => $gradinginstance->get_data('id'), 'criterionid' => $criterion->id]); - - if ($existinggrade) { - // Update existing grade. - $existinggrade->levelid = $grade['levelid']; - $existinggrade->remark = $grade['remark']; - $existinggrade->raterid = $USER->id; // Assuming the current user is the rater. - $DB->update_record('gradingform_rubric_fillings', $existinggrade); - } else { - // Insert new grade. - $newgrade = new \stdClass(); - $newgrade->instanceid = $gradinginstance->get_data('id'); // Use correct instanceid - $newgrade->criterionid = $criterion->id; - $newgrade->levelid = $grade['levelid']; - $newgrade->remark = $grade['remark']; - $newgrade->raterid = $USER->id; // Assuming the current user is the rater. - $DB->insert_record('gradingform_rubric_fillings', $newgrade); - } - } - - // Create a new object for updating the grading instance's status. - $instanceupdate = new \stdClass(); - $instanceupdate->id = $gradinginstance->get_data('id'); - $instanceupdate->status = 1; // 1 means "finalized". - $DB->update_record('grading_instances', $instanceupdate); - - // Delete any unsubmitted (status = 0) grading instances for this user and item. - $DB->delete_records('grading_instances', ['itemid' => $itemid, 'raterid' => $USER->id, 'status' => 0]); - - // Get the maximum grade for the assignment - $assignment = $DB->get_record('assign', ['id' => $assignmentid], '*', MUST_EXIST); - $maxgrade = $assignment->grade; // Maximum possible grade - - // Calculate the final grade as a proportion of the total score to the maximum possible score. - $finalgradevalue = ($total_score / $max_score) * $maxgrade; - - // Update the grade in the assign_grades table to reflect the final state. - $finalgrade = $DB->get_record('assign_grades', ['id' => $itemid], '*', MUST_EXIST); - $finalgrade->grader = $USER->id; - $finalgrade->grade = $finalgradevalue; - $finalgrade->timemodified = time(); - $DB->update_record('assign_grades', $finalgrade); - - $gradeupdate = new \stdClass(); - $gradeupdate->id = $finalgrade->id; - $gradeupdate->assignment = $assignmentid; - $gradeupdate->userid = $userid; - $gradeupdate->grade = $finalgradevalue; - $gradeupdate->rawgrade = $finalgradevalue; - $gradeupdate->grader = $USER->id; - $gradeupdate->timecreated = $finalgrade->timecreated; - $gradeupdate->timemodified = time(); - $assign = new \assign($context, null, null); - $assign->update_grade($gradeupdate); - - return true; - } - -} diff --git a/team_a/MoodlePlugin/learninglens/db/.DS_Store b/team_a/MoodlePlugin/learninglens/db/.DS_Store deleted file mode 100644 index 5008ddfc..00000000 Binary files a/team_a/MoodlePlugin/learninglens/db/.DS_Store and /dev/null differ diff --git a/team_a/MoodlePlugin/learninglens/db/access.php b/team_a/MoodlePlugin/learninglens/db/access.php deleted file mode 100644 index 62fabc80..00000000 --- a/team_a/MoodlePlugin/learninglens/db/access.php +++ /dev/null @@ -1,15 +0,0 @@ - array( - 'riskbitmask' => RISK_SPAM | RISK_XSS, - - 'captype' => 'write', - 'contextlevel' => CONTEXT_COURSE, - 'archetypes' => array( - 'teacher' => CAP_ALLOW, - 'editingteacher' => CAP_ALLOW, - ), - ), -); \ No newline at end of file diff --git a/team_a/MoodlePlugin/learninglens/db/services.php b/team_a/MoodlePlugin/learninglens/db/services.php deleted file mode 100644 index bb94928c..00000000 --- a/team_a/MoodlePlugin/learninglens/db/services.php +++ /dev/null @@ -1,160 +0,0 @@ - [ - 'classname' => 'local_learninglens\external\create_quiz', - 'description' => 'Create a quiz in a specified course', - 'type' => 'write', - 'ajax' => true, - 'capabilities' => 'mod/quiz:addinstance', - 'services' => [ - MOODLE_OFFICIAL_MOBILE_SERVICE, - ] - ], - - 'local_learninglens_import_questions' => [ - 'classname' => 'local_learninglens\external\import_questions', - 'description' => 'Import XML questions to a course question bank', - 'type' => 'write', - 'ajax' => true, - 'capabilities' => 'moodle/question:add', - 'services' => [ - MOODLE_OFFICIAL_MOBILE_SERVICE, - ] - ], - 'local_learninglens_add_type_randoms_to_quiz' => [ - 'classname' => 'local_learninglens\external\add_type_randoms_to_quiz', - 'description' => 'Add questions of type random to quiz from category', - 'type' => 'write', - 'ajax' => true, - 'capabilities' => 'mod/quiz:manage', - 'services' => [ - MOODLE_OFFICIAL_MOBILE_SERVICE, - ] - ], - 'local_learninglens_get_rubric' => [ - 'classname' => 'local_learninglens\external\get_rubric', - 'description' => 'Returns instances of grading forms including rubrics.', - 'type' => 'read', - 'ajax' => true, - 'capabilities' => 'moodle/grade:view', - 'services' => [ - MOODLE_OFFICIAL_MOBILE_SERVICE, - ] - ], - 'local_learninglens_create_assignment' => [ - 'classname' => 'local_learninglens\external\create_assignment', - 'description' => 'Creates an assignment and optionally attach a rubric.', - 'type' => 'write', - 'ajax' => true, - 'capabilities' => 'moodle/course:manageactivities', - 'services' => [ - MOODLE_OFFICIAL_MOBILE_SERVICE, - ] - ], - 'local_learninglens_get_rubric_grades' => [ - 'classname' => 'local_learninglens\external\get_rubric_grades', - 'description' => 'Gets rubric grades for a specific submission.', - 'type' => 'read', - 'ajax' => true, - 'capabilities' => 'mod/assign:grade', - 'services' => [ - MOODLE_OFFICIAL_MOBILE_SERVICE, - ] - ], - 'local_learninglens_write_rubric_grades' => [ - 'classname' => 'local_learninglens\external\write_rubric_grades', - 'description' => 'Sets rubric grades for a specific submission.', - 'type' => 'write', - 'ajax' => true, - 'capabilities' => 'mod/assign:grade', - 'services' => [ - MOODLE_OFFICIAL_MOBILE_SERVICE, - ] - ], - 'local_learninglens_write_grades' => [ - 'classname' => 'local_learninglens\external\write_grades', - 'description' => 'Sets grades for a specific submission.', - 'type' => 'write', - 'ajax' => true, - 'capabilities' => 'mod/assign:grade', - 'services' => [ - MOODLE_OFFICIAL_MOBILE_SERVICE, - ] - ], - 'local_learninglens_create_lesson' => [ - 'classname' => 'local_learninglens\external\create_lesson', - 'description' => 'Creates a new lesson in a Moodle course and links it to the course modules', - 'type' => 'write', - 'ajax' => true, - 'capabilities' => 'mod/lesson:addinstance', - 'services' => [ - MOODLE_OFFICIAL_MOBILE_SERVICE, - ] - ], - 'local_learninglens_get_questions_from_quiz' => [ - 'classname' => 'local_learninglens\external\get_questions_from_quiz', - 'description' => 'Fetches questions linked to a quiz ID', - 'type' => 'read', - 'ajax' => true, - 'capabilities'=> 'mod/quiz:view', - 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], - ], - 'local_learninglens_add_quiz_override' => [ - 'classname' => 'local_learninglens\external\add_quiz_override', - 'description' => 'Adds an override to a quiz for a user or group.', - 'type' => 'write', - 'ajax' => true, - 'capabilities'=> 'mod/quiz:manage', - 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], - ], - 'local_learninglens_add_essay_override' => [ - 'classname' => 'local_learninglens\external\add_essay_override', - 'description' => 'Adds an override to a essay for a user or group.', - 'type' => 'write', - 'ajax' => true, - 'capabilities'=> 'mod/assign:manage', - 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], - ], - 'local_learninglens_get_lesson_plans_by_course' => [ - 'classname' => 'local_learninglens\external\get_lesson_plans_by_course', - 'description' => 'Retrieves lesson plans associated with a given course ID.', - 'type' => 'read', - 'ajax' => true, - 'capabilities' => 'mod/lesson:view', - 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] // Enables use in the Moodle Mobile App - ], - 'local_learninglens_delete_lesson_plan' => [ - 'classname' => 'local_learninglens\external\delete_lesson_plan', - 'description' => 'Deletes a lesson plan by ID.', - 'type' => 'write', - 'ajax' => true, - 'capabilities' => 'mod/lesson:manage', - 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] - ], - 'local_learninglens_update_lesson_plan' => [ - 'classname' => 'local_learninglens\external\update_lesson_plan', - 'description' => 'Updates an existing lesson plan by ID.', - 'type' => 'write', - 'ajax' => true, - 'capabilities' => 'mod/lesson:manage', - 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] - ], - 'local_learninglens_get_all_overrides' => [ - 'classname' => 'local_learninglens\external\get_all_overrides', - 'description' => 'Gets overrides for both quiz and essay', - 'type' => 'read', - 'ajax' => true, - 'capabilities' => 'mod/quiz:view, mod/assign:view', - 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE] - ], - 'local_learninglens_get_question_stats_from_quiz' => [ - 'classname' => 'local_learninglens\external\get_question_stats_from_quiz', - 'description' => 'Fetches questions + stats (correct/incorrect counts) for a quiz', - 'type' => 'read', - 'ajax' => true, - 'capabilities'=> 'mod/quiz:view', - 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], - ], -]; \ No newline at end of file diff --git a/team_a/MoodlePlugin/learninglens/lang/en/local_learninglens.php b/team_a/MoodlePlugin/learninglens/lang/en/local_learninglens.php deleted file mode 100644 index 05c1c472..00000000 --- a/team_a/MoodlePlugin/learninglens/lang/en/local_learninglens.php +++ /dev/null @@ -1,23 +0,0 @@ -version = 2025031414; // YYYYMMDDHH (Year Month Day 24-hr time). -$plugin->requires = 2022112800; // YYYYMMDDHH (The release version of Moodle 4.1). -$plugin->component = 'local_learninglens'; // Name of your plugin (used for diagnostics). -$plugin->maturity = MATURITY_ALPHA; -?> \ No newline at end of file diff --git a/team_a/MoodlePlugin/local_adminer_moodle45_2025021700.zip b/team_a/MoodlePlugin/local_adminer_moodle45_2025021700.zip deleted file mode 100644 index 63ab56c5..00000000 Binary files a/team_a/MoodlePlugin/local_adminer_moodle45_2025021700.zip and /dev/null differ diff --git a/team_a/README.md b/team_a/README.md deleted file mode 100644 index 28196599..00000000 --- a/team_a/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# spring2025 -Fall SWEN 670 Cohort - -Team A (Learning Lens) - LLM Integrated Software Application for K-12 Education - - -Members: -Kevin Watts (Team Lead, Technical Writer) -Dusty McKinnon (Technical Writer, Lead QA Engineer) -Derek Sappington (Business Analyst, Software Engineer) -Daniel Diep (Software Engineer / Programmer, Testing) -Andrew Hammes (Business Analyst, Software Developer) -Dinesh Ghimire (Software Developer, Tester) -Nathaniel Boyd (Test Server Operator, System Admin) diff --git a/team_a/pubspec.yaml b/team_a/pubspec.yaml deleted file mode 100644 index 72ae07f9..00000000 --- a/team_a/pubspec.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: learninglens_app -description: A new Flutter project. - -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -version: 0.0.1+1 - -environment: - sdk: ^3.1.1 - -dependencies: - flutter: - sdk: flutter - flutter_dotenv: ^5.1.0 - xml: ^6.2.2 - http: ^1.2.1 - flutter_quill: ^10.8.0 - - english_words: ^4.0.0 - provider: ^6.0.0 - shared_preferences: ^2.2.0 - - -dev_dependencies: - flutter_test: - sdk: flutter - - flutter_lints: ^2.0.0 - -flutter: - uses-material-design: true \ No newline at end of file diff --git a/team_a/teamA/.example.env b/team_a/teamA/.example.env deleted file mode 100644 index 06c3adc5..00000000 --- a/team_a/teamA/.example.env +++ /dev/null @@ -1,11 +0,0 @@ -# This file contains the required environment variables for the application. -# To configure your environment, create a new `.env` file in the project root, -# copy these variables, and populate them with the appropriate values. - -openai_apikey= -MOODLE_USERNAME= -MOODLE_PASSWORD= -MOODLE_URL= -claudeApiKey= -perplexity_apikey= -GOOGLE_CLIENT_ID= \ No newline at end of file diff --git a/team_a/teamA/.gitignore b/team_a/teamA/.gitignore deleted file mode 100644 index 3af0d322..00000000 --- a/team_a/teamA/.gitignore +++ /dev/null @@ -1,51 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# .metadata file from flutter create -.metadata - -# widget_test.dart file from flutter create -test/widget_test.dart - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ - -/assets/api-keys.json - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json -macos/* -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release diff --git a/team_a/teamA/.gitkeep b/team_a/teamA/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/team_a/teamA/README.md b/team_a/teamA/README.md deleted file mode 100644 index a35fcbf3..00000000 --- a/team_a/teamA/README.md +++ /dev/null @@ -1,18 +0,0 @@ -#learninglens_app - -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. - -Test \ No newline at end of file diff --git a/team_a/teamA/analysis_options.yaml b/team_a/teamA/analysis_options.yaml deleted file mode 100644 index fd6dbde2..00000000 --- a/team_a/teamA/analysis_options.yaml +++ /dev/null @@ -1,14 +0,0 @@ -analyzer: - errors: - deprecated_member_use: ignore -include: package:flutter_lints/flutter.yaml - -linter: - rules: - avoid_print: false - prefer_const_constructors_in_immutables: false - prefer_const_constructors: false - prefer_const_literals_to_create_immutables: false - prefer_final_fields: false - unnecessary_breaks: true - use_key_in_widget_constructors: false diff --git a/team_a/teamA/assets/criteria.json b/team_a/teamA/assets/criteria.json deleted file mode 100644 index b2a4a34a..00000000 --- a/team_a/teamA/assets/criteria.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "Criteria": [ - { - "Name": "Introduction Effectiveness", - "Description": "clarity and effectiveness in the introductory paragraph by presenting the essay's main argument or focus." - }, - { - "Name": "Thesis Statement Strength", - "Description": "precision, clarity, and relevance in the thesis statement, effectively guiding the essay." - }, - { - "Name": "Conclusion Quality", - "Description": "effectiveness in the conclusion by summarizing key points and reinforcing the thesis." - }, - { - "Name": "Depth of Analysis", - "Description": "depth and complexity in analyzing the topic or themes within the essay." - }, - { - "Name": "Understanding of Context", - "Description": "understanding of the historical, cultural, or social context related to the essay's topic." - }, - { - "Name": "Logical Flow", - "Description": "logical progression of ideas and arguments throughout the essay." - }, - { - "Name": "Paragraph Structure", - "Description": "coherence and clarity in individual paragraphs, with clear topic sentences and supporting details." - }, - { - "Name": "Transition Between Ideas", - "Description": "effectiveness in transitioning between ideas or paragraphs within the essay." - }, - { - "Name": "Originality of Thought", - "Description": "originality and creativity in their analysis or argument throughout the essay." - }, - { - "Name": "Use of Literary Devices", - "Description": "identification and discussion of literary devices such as symbolism, metaphors, or imagery." - }, - { - "Name": "Grammar and Syntax", - "Description": "use of grammar, punctuation, and sentence structure throughout the essay." - }, - { - "Name": "Adherence to Assignment Guidelines", - "Description": "adherence to the assignment's requirements, including length, topic, and formatting." - }, - { - "Name": "Cohesion of Argument", - "Description": "strength and consistency in the main argument across all sections of the essay." - }, - { - "Name": "Relevance of Evidence", - "Description": "relevance and appropriateness in the supporting evidence used to back up claims." - }, - { - "Name": "Critical Thinking", - "Description": "critical thinking skills in evaluating texts, arguments, or perspectives." - }, - { - "Name": "Depth of Research", - "Description": "quality and depth of research, using credible sources where applicable." - }, - { - "Name": "Word Choice", - "Description": "precision, sophistication, and appropriateness in their vocabulary use throughout the essay." - }, - { - "Name": "Persuasiveness", - "Description": "persuasiveness in convincing the reader of their argument or perspective." - }, - { - "Name": "Citation and Referencing", - "Description": "use of citation formats and accuracy in referencing sources (APA, MLA, etc.)." - }, - { - "Name": "Engagement with Counterarguments", - "Description": "ability to acknowledge, engage with, and refute opposing viewpoints or counterarguments." - } - ] -} \ No newline at end of file diff --git a/team_a/teamA/assets/login_image.png b/team_a/teamA/assets/login_image.png deleted file mode 100644 index b56d6551..00000000 Binary files a/team_a/teamA/assets/login_image.png and /dev/null differ diff --git a/team_a/teamA/devtools_options.yaml b/team_a/teamA/devtools_options.yaml deleted file mode 100644 index fa0b357c..00000000 --- a/team_a/teamA/devtools_options.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: This file stores settings for Dart & Flutter DevTools. -documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states -extensions: diff --git a/team_a/teamA/lib/Api/experimental/README.md b/team_a/teamA/lib/Api/experimental/README.md deleted file mode 100644 index 47b7adce..00000000 --- a/team_a/teamA/lib/Api/experimental/README.md +++ /dev/null @@ -1 +0,0 @@ -This folder is for experimental ideas only—not for production. It’s a proof of concept showing how an AI assistant can fetch info like students, courses, and quizzes from our app (Moodle-based). Think of it as a sandbox. \ No newline at end of file diff --git a/team_a/teamA/lib/Api/experimental/assistant/textbased_function_caller.dart b/team_a/teamA/lib/Api/experimental/assistant/textbased_function_caller.dart deleted file mode 100644 index 874b1102..00000000 --- a/team_a/teamA/lib/Api/experimental/assistant/textbased_function_caller.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:learninglens_app/Api/lms/lms_interface.dart'; -import 'package:learninglens_app/beans/course.dart'; -import 'package:learninglens_app/beans/participant.dart'; -import 'package:learninglens_app/beans/quiz.dart'; - -class TextBasedFunctionCaller { - final LmsInterface lmsService; - - TextBasedFunctionCaller(this.lmsService); - - /// Dispatch the function call based on [functionName] and [args]. - Future callFunctionByName( - String functionName, Map args) async { - print('${functionName}, ${args}'); - try { - switch (functionName) { - case "getUserCourses": - final courses = await lmsService.getUserCourses(); - return _formatCourses(courses); - - case "getQuizzes": - if (!args.containsKey("courseID")) { - return "Error: Missing 'courseID'."; - } - // Try parse courseID - final courseID = int.tryParse(args["courseID"]); - if (courseID == null) { - return "Error: 'courseID' must be an integer."; - } - - // Check if quizTopicId was provided - if (!args.containsKey("quizTopicId") || - args["quizTopicId"] == null || - args["quizTopicId"].isEmpty) { - // Not provided => pass null to getQuizzes - final quizzes = await lmsService.getQuizzes(courseID, topicId: null); - return _formatQuizzes(quizzes); - } else { - // Provided => parse it - final int? quizTopicId = int.tryParse(args["quizTopicId"]); - if (quizTopicId == null) { - return "Error: 'quizTopicId' must be an integer if provided."; - } - final quizzes = - await lmsService.getQuizzes(courseID, topicId: quizTopicId); - return _formatQuizzes(quizzes); - } - - case "getQuizGradesForParticipants": - if (!args.containsKey("courseId") || !args.containsKey("quizId")) { - return "Error: Missing courseId or quizId."; - } - final courseId = args["courseId"]; - final quizId = int.parse(args["quizId"]); - final participants = - await lmsService.getQuizGradesForParticipants(courseId, quizId); - return _formatGrades(participants); - - case "getCourseParticipants": - if (!args.containsKey("courseId")) { - return "Error: Missing courseId."; - } - final courseId = args["courseId"]; - final participants = await lmsService.getCourseParticipants(courseId); - return _formatParticipants(participants); - - default: - return "Error: Unknown function '$functionName'."; - } - } catch (e) { - // For your real app, handle or log the exception in detail - return "Error: Exception occurred calling '$functionName': $e"; - } - } - - // Helper: format a list of courses - String _formatCourses(List courses) { - if (courses.isEmpty) return "No enrolled courses found."; - return courses - .map((c) => - "Course: ${c.fullName} (ID: ${c.id}, quizTopicId: ${c.quizTopicId})") - .join("\n"); - } - - // Helper: format a list of quizzes - String _formatQuizzes(List quizzes) { - if (quizzes.isEmpty) return "No quizzes found."; - return quizzes.map((q) => "Quiz: ${q.name} (ID: ${q.id})").join("\n"); - } - - // Helper: format quiz grades for participants - String _formatGrades(List participants) { - if (participants.isEmpty) return "No participants or grades found."; - return participants.map((p) { - final gradeStr = - (p.avgGrade != null) ? p.avgGrade.toString() : "No grade"; - return "Student: ${p.fullname} (ID: ${p.id}), Grade: $gradeStr"; - }).join("\n"); - } - - // Helper: format participants list - String _formatParticipants(List participants) { - if (participants.isEmpty) return "No participants found."; - return participants - .map((p) => - "Student: ${p.fullname} (ID: ${p.id}) Roles: ${p.roles.join(', ')}") - .join("\n"); - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/Api/experimental/assistant/textbased_function_caller_view.dart b/team_a/teamA/lib/Api/experimental/assistant/textbased_function_caller_view.dart deleted file mode 100644 index e2cb08c5..00000000 --- a/team_a/teamA/lib/Api/experimental/assistant/textbased_function_caller_view.dart +++ /dev/null @@ -1,218 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:learninglens_app/Api/experimental/assistant/textbased_function_caller.dart'; -import 'package:learninglens_app/Api/experimental/assistant/textbased_llm_client.dart'; -import 'package:learninglens_app/Api/llm/enum/llm_enum.dart'; -import 'package:learninglens_app/Api/llm/grok_api.dart'; -import 'package:learninglens_app/Api/llm/llm_api_modules_base.dart'; -import 'package:learninglens_app/Api/llm/openai_api.dart'; -import 'package:learninglens_app/Api/llm/perplexity_api.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:learninglens_app/Controller/custom_appbar.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; - -class TextBasedFunctionCallerView extends StatefulWidget { - const TextBasedFunctionCallerView({Key? key}) : super(key: key); - - @override - _TextBasedFunctionCallerViewState createState() => - _TextBasedFunctionCallerViewState(); -} - -class _TextBasedFunctionCallerViewState - extends State { - final TextEditingController _controller = TextEditingController(); - final FocusNode _focusNode = FocusNode(); - final List> _messages = []; - late TextBasedLLMClient _chatGPT; - bool _isThinking = false; // Tracks if the bot is thinking - - LlmType selectedLLM = LlmType.GROK; //default to chatgpt - late LLM llm; - - @override - void initState() { - super.initState(); - - llm = getLLM(); - - _chatGPT = TextBasedLLMClient( - llm, - TextBasedFunctionCaller(LmsFactory.getLmsService()), - ); - } - - // grabs the selected LLM. - LLM getLLM() { - final aiModel; - if (selectedLLM == LlmType.CHATGPT) { - aiModel = OpenAiLLM(LocalStorageService.getOpenAIKey()); - } else if (selectedLLM == LlmType.GROK) { - aiModel = GrokLLM(LocalStorageService.getGrokKey()); - } else if (selectedLLM == LlmType.PERPLEXITY) { - // aiModel = OpenAiLLM(perplexityApiKey); - aiModel = PerplexityLLM(LocalStorageService.getPerplexityKey()); - } else { - // default - aiModel = OpenAiLLM(LocalStorageService.getOpenAIKey()); - } - return aiModel; - } - - void _sendMessage() async { - if (_controller.text.isEmpty) return; - - setState(() { - _messages.add({"sender": "user", "text": _controller.text}); - _isThinking = true; // Show thinking dots - }); - - final userMessage = _controller.text; - _controller.clear(); - - // Add a "thinking" message - // setState(() { - // _messages.add({"sender": "bot", "text": "..."}); - // }); - - String response = await _chatGPT.sendMessage(userMessage); - - setState(() { - // _messages.removeLast(); // Remove "thinking" message - _messages.add({"sender": "bot", "text": response}); - _isThinking = false; // Bot has finished responding - }); - - _focusNode.requestFocus(); // Refocus input field - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: 'EduLense Assistant (Beta textbased)', - userprofileurl: LmsFactory.getLmsService().profileImage ?? '', - ), - body: Column( - children: [ - Expanded( - child: ListView.builder( - itemCount: _messages.length, - itemBuilder: (context, index) { - final message = _messages[index]; - bool isUser = message["sender"] == "user"; - - return Padding( - padding: EdgeInsets.symmetric(vertical: 5, horizontal: 10), - child: Row( - mainAxisAlignment: isUser - ? MainAxisAlignment.end - : MainAxisAlignment.start, - children: [ - if (!isUser) ...[ - CircleAvatar( - backgroundColor: Colors.grey[200], - child: Icon(Icons.android, color: Colors.blueAccent), - ), - SizedBox(width: 8), - ], - Flexible( - child: Container( - padding: EdgeInsets.all(12), - decoration: BoxDecoration( - color: - isUser ? Colors.blueAccent : Colors.grey[300], - borderRadius: BorderRadius.circular(15), - ), - child: Text( - message["text"]!, - style: TextStyle( - color: isUser ? Colors.white : Colors.black, - fontSize: 16, - ), - ), - ), - ), - if (isUser) ...[ - SizedBox(width: 8), - CircleAvatar( - backgroundColor: Colors.grey[200], - child: Icon(Icons.person, color: Colors.blueAccent), - ), - ], - ], - ), - ); - }, - ), - ), - if (_isThinking) - Padding( - padding: const EdgeInsets.all(8.0), - child: CircularProgressIndicator(), // Loading indicator - ), - Padding( - padding: const EdgeInsets.all(10.0), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _controller, - focusNode: _focusNode, - decoration: InputDecoration( - hintText: "Ask ${selectedLLM.displayName}...", - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(20), - ), - ), - onSubmitted: (value) => _sendMessage(), - ), - ), - SizedBox(width: 8), - ClipOval( - child: Material( - color: Colors.blueAccent, - child: InkWell( - onTap: _sendMessage, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Icon(Icons.send, color: Colors.white), - ), - ), - ), - ), - DropdownButton( - value: selectedLLM, - onChanged: (LlmType? newValue) { - setState(() { - selectedLLM = newValue!; - llm = getLLM(); - - _chatGPT = TextBasedLLMClient( - llm, - TextBasedFunctionCaller(LmsFactory.getLmsService()), - ); - - }); - }, - items: LlmType.values.map((LlmType llm) { - return DropdownMenuItem( - value: llm, - enabled: LocalStorageService.userHasLlmKey(llm), - child: Text( - llm.displayName, - style: TextStyle( - color: LocalStorageService.userHasLlmKey(llm) - ? Colors.black87 - : Colors.grey, - ), - ), - ); - }).toList()), - ], - ), - ), - ], - ), - ); - } -} diff --git a/team_a/teamA/lib/Api/experimental/assistant/textbased_llm_client.dart b/team_a/teamA/lib/Api/experimental/assistant/textbased_llm_client.dart deleted file mode 100644 index 6e781877..00000000 --- a/team_a/teamA/lib/Api/experimental/assistant/textbased_llm_client.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:learninglens_app/Api/experimental/assistant/textbased_function_caller.dart'; -import 'package:learninglens_app/Api/llm/llm_api_modules_base.dart'; -import 'package:learninglens_app/Api/llm/prompt_engine.dart'; -import 'package:learninglens_app/services/api_service.dart'; - -// Replicate the functionality used in chatgpt_client, but swap over to prompt engineering instead of the function caller. -// This code gears the development for the assistant to be more generic in terms of which llm is used rather than relying on -// functionality developement by OpenAI. -class TextBasedLLMClient { - final LLM llm; - final TextBasedFunctionCaller functionCaller; - - /// Maintains the entire conversation (system, user, assistant). - final List> _conversation = []; - - TextBasedLLMClient(this.llm, this.functionCaller) { - // Add system instructions once at the beginning - _conversation.add({ - "role": "system", - "content": PromptEngine.prompt_assistant - }); - } - - /// Send a user message (multi-turn). If the LLM keeps calling functions, - /// we'll keep looping until we get a plain text answer. - Future sendMessage(String userMessage) async { - // 1) Add the user's message to conversation - _conversation.add({ - "role": "user", - "content": userMessage, - }); - - // 2) Multi-turn loop: keep calling the LLM until we get a non-CALL response - while (true) { - final llmReply = await _callLLM(_conversation); - if (llmReply == null || llmReply.isEmpty) { - return "No response from the LLM."; - } - - final trimmedReply = llmReply.trim(); - - // If the model requests a function call - if (trimmedReply.startsWith("CALL ")) { - // Handle the function call and add the result to the conversation - final functionResult = await _handleFunctionCall(trimmedReply); - - // Insert the function result as an "assistant" message - _conversation.add({ - "role": "assistant", - "content": functionResult, - }); - - // Then loop again so the LLM can see that result and possibly call another function - } else { - // It's a final textual answer - _conversation.add({ - "role": "assistant", - "content": trimmedReply, - }); - return trimmedReply; - } - } - } - - /// Calls the OpenAI Chat Completion API with the entire conversation so far - Future _callLLM(List> conversation) async { - - - final response = await ApiService().httpPost( - Uri.parse(llm.url), - headers: { - "Authorization": 'Bearer ${llm.apiKey}', - "Content-Type": "application/json", - }, - body: jsonEncode({ - "model": llm.model, - "messages": conversation.map((m) { - return {"role": m["role"], "content": m["content"]}; - }).toList(), - "temperature": 0.7, - "top_p": 0.9, - }), - ); - - if (response.statusCode != 200) { - return "Error from LLM: ${response.statusCode} => ${response.body}"; - } - - final jsonData = jsonDecode(response.body); - if (jsonData["choices"] == null || jsonData["choices"].isEmpty) { - return null; - } - - return jsonData["choices"][0]["message"]["content"]; - } - - /// Parses a "CALL functionName(...)" string, calls the local function, returns its result - Future _handleFunctionCall(String callString) async { - // Example: "CALL getQuizzes(courseID=101, quizTopicId=201)" - final afterCall = callString.substring(5).trim(); // remove "CALL " - final openParenIndex = afterCall.indexOf("("); - if (openParenIndex == -1) { - return "Error: malformed function call, missing '('"; - } - - final functionName = afterCall.substring(0, openParenIndex).trim(); - - final closeParenIndex = afterCall.indexOf(")", openParenIndex); - if (closeParenIndex == -1) { - return "Error: malformed function call, missing ')'"; - } - - final argsString = afterCall.substring(openParenIndex + 1, closeParenIndex).trim(); - - // Parse key=value pairs - final Map argsMap = {}; - if (argsString.isNotEmpty) { - final pairs = argsString.split(","); - for (final pair in pairs) { - final parts = pair.split("="); - if (parts.length == 2) { - final key = parts[0].trim(); - final val = parts[1].trim(); - argsMap[key] = val; // Keep as string; parse to int if you like - } - } - } - - // Actually call the function - final result = await functionCaller.callFunctionByName(functionName, argsMap); - return result; - } -} diff --git a/team_a/teamA/lib/Api/llm/claudeai_api.dart b/team_a/teamA/lib/Api/llm/claudeai_api.dart deleted file mode 100644 index c93714b7..00000000 --- a/team_a/teamA/lib/Api/llm/claudeai_api.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'package:learninglens_app/services/api_service.dart'; - -class ClaudeAiAPI { - final String claudeAiKey; - ClaudeAiAPI(this.claudeAiKey); - - Map convertHttpRespToJson(String httpResponseString) { - return (json.decode(httpResponseString) as Map); - } - - String getPostBody(String queryMessage) { - return jsonEncode({ - 'model': 'claude-3-5-sonnet-20240620', - 'max_tokens': 1024, - 'system': 'Be precise and concise', - 'messages': [ - {'role': 'user', 'content': queryMessage} - ] - }); - } - - Map getPostHeaders() { - return ({ - 'anthropic-version': '2023-06-01', - 'content-type': 'application/json', - 'anthropic-dangerous-direct-browser-access': 'true', - 'x-api-key': claudeAiKey, - }); - } - - Uri getPostUrl() => Uri.https('api.anthropic.com', '/v1/messages'); - - Future postMessage( - Uri url, Map postHeaders, Object postBody) async { - final httpPackageResponse = - await ApiService().httpPost(url, headers: postHeaders, body: postBody); - - if (httpPackageResponse.statusCode != 200) { - print('Failed to retrieve the http package!'); - print('statusCode : ${httpPackageResponse.statusCode}'); - print('Reason: ${httpPackageResponse.body}'); - - return ""; - } - - return httpPackageResponse.body; - } - - List parseQueryResponse(String resp) { - // ignore: prefer_adjacent_string_concatenation - String quizRegExp = - // r'(<\?xml.*?\?>\s*(\s*.*?\s*.*?\s*(.*?)\s*.*?(\s*.*?)+\s*\s*.*?\s*(.*?)\s*.*?)+\s*)'; - r'(<\?xml.*?\?>\s*.*?)'; - - RegExp exp = RegExp(quizRegExp); - String respNoNewlines = resp.replaceAll('\n', ''); - Iterable matches = exp.allMatches(respNoNewlines); - List parsedResp = []; - - print("Parsing the query response - matches: $matches"); - - for (final m in matches) { - if (m.group(0) != null) { - parsedResp.add(m.group(0)!); - - print("This is a match : ${m.group(0)}"); - print("Number of groups in the match: ${m.groupCount}"); - print("parsedResp : $parsedResp"); - } - } - - return parsedResp; - } - - Future postToLlm(String queryPrompt) async { - var resp = ""; - - // use the following test query so Perplexity doesn't charge - // 'How many stars are there in our galaxy?' - if (queryPrompt.isNotEmpty) { - resp = await queryAI(queryPrompt); - } - return resp; - } - - Future queryAI(String query) async { - final postHeaders = getPostHeaders(); - final postBody = getPostBody(query); - final httpPackageUrl = getPostUrl(); - - final httpPackageRespString = - await postMessage(httpPackageUrl, postHeaders, postBody); - - final httpPackageResponseJson = - convertHttpRespToJson(httpPackageRespString); - - var retResponse = ""; - for (var respChoice in httpPackageResponseJson['content']) { - retResponse += respChoice['text']; - } - // print("In queryAI - content : $retResponse"); - return retResponse; - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/Api/llm/enum/llm_enum.dart b/team_a/teamA/lib/Api/llm/enum/llm_enum.dart deleted file mode 100644 index fc48cf81..00000000 --- a/team_a/teamA/lib/Api/llm/enum/llm_enum.dart +++ /dev/null @@ -1,8 +0,0 @@ -enum LlmType { - CHATGPT('ChatGPT'), - PERPLEXITY('Perplexity'), - GROK('Grok'); - - final String displayName; - const LlmType(this.displayName); -} \ No newline at end of file diff --git a/team_a/teamA/lib/Api/llm/grok_api.dart b/team_a/teamA/lib/Api/llm/grok_api.dart deleted file mode 100644 index f197b933..00000000 --- a/team_a/teamA/lib/Api/llm/grok_api.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:learninglens_app/Api/llm/llm_api_modules_base.dart'; -import 'package:learninglens_app/services/api_service.dart'; - -class GrokLLM implements LLM { - @override - final String apiKey; - @override - final String url = 'https://api.x.ai/v1/chat/completions'; - @override - final String model = 'grok-2-latest'; - - GrokLLM(this.apiKey); - - Map convertHttpRespToJson(String httpResponseString) { - return (json.decode(httpResponseString) as Map); - } - - /// - /// - /// - String getPostBody(String queryMessage) { - return jsonEncode({ - 'model': model, - 'messages': [ - {'role': 'system', 'content': 'Be precise and concise'}, - {'role': 'user', 'content': queryMessage} - ] - }); - } - - /// - /// - /// - Map getPostHeaders() { - return ({ - 'accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $apiKey', - }); - } - - /// - /// - /// - Uri getPostUrl() => Uri.https('api.x.ai', '/v1/chat/completions'); - - /// - /// - /// - Future postMessage( - Uri url, Map postHeaders, Object postBody) async { - final httpPackageResponse = - await ApiService().httpPost(url, headers: postHeaders, body: postBody); - - print(url); - print(postHeaders); - print(postBody); - - if (httpPackageResponse.statusCode != 200) { - print('Failed to retrieve the http package!'); - print('statusCode : ${httpPackageResponse.statusCode}'); - print('Reason: ${httpPackageResponse.body}'); - - return ""; - } - - print("In postmessage : ${httpPackageResponse.body}"); - return httpPackageResponse.body; - } - - List parseQueryResponse(String resp) { - // ignore: prefer_adjacent_string_concatenation - String quizRegExp = - // r'(<\?xml.*?\?>\s*(\s*.*?\s*.*?\s*(.*?)\s*.*?(\s*.*?)+\s*\s*.*?\s*(.*?)\s*.*?)+\s*)'; - r'(<\?xml.*?\?>\s*.*?)'; - - RegExp exp = RegExp(quizRegExp); - String respNoNewlines = resp.replaceAll('\n', ''); - Iterable matches = exp.allMatches(respNoNewlines); - List parsedResp = []; - - print("Parsing the query response - matches: $matches"); - - for (final m in matches) { - if (m.group(0) != null) { - parsedResp.add(m.group(0)!); - - print("This is a match : ${m.group(0)}"); - print("Number of groups in the match: ${m.groupCount}"); - print("parsedResp : $parsedResp"); - } - } - - return parsedResp; - } - - /// - /// - /// - Future postToLlm(String queryPrompt) async { - var resp = ""; - - // use the following test query so Perplexity doesn't charge - // 'How many stars are there in our galaxy?' - if (queryPrompt.isNotEmpty) { - resp = await queryAI(queryPrompt); - } - return resp; - } - - /// - /// - /// - Future queryAI(String query) async { - final postHeaders = getPostHeaders(); - final postBody = getPostBody(query); - final httpPackageUrl = getPostUrl(); - - final httpPackageRespString = - await postMessage(httpPackageUrl, postHeaders, postBody); - - final httpPackageResponseJson = - convertHttpRespToJson(httpPackageRespString); - - var retResponse = ""; - for (var respChoice in httpPackageResponseJson['choices']) { - retResponse += respChoice['message']['content']; - } - print("In queryAI - content : $retResponse"); - return retResponse; - } - - 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); - - // Check for successful response - if (response.statusCode == 200) { - var data = jsonDecode(response.body); - return data['choices'][0]['message']['content'] - .trim(); // Return the chat response - } else { - // Log the error response and handle failure cases - print('Failed to fetch response. Status code: ${response.statusCode}'); - print('Response body: ${response.body}'); - return 'Sorry, I couldn’t fetch a response. Please try again.'; - } - } catch (error) { - // Log and handle connection or parsing errors - print('Error occurred: $error'); - return 'An error occurred. Please check your internet connection and try again.'; - } - } - - - - - @override - Future generate(String prompt) async { - final url = Uri.parse(this.url); // Hypothetical endpoint - final headers = { - 'Authorization': 'Bearer $apiKey', - 'Content-Type': 'application/json', - }; - final body = jsonEncode({ - 'model': model, // Use the configurable model - 'messages': [ - {'role': 'user', 'content': prompt}, - ], - 'max_tokens': 500, // Limit response length - }); - - final response = await http.post(url, headers: headers, body: body); - if (response.statusCode != 200) { - throw Exception('Grok API error: ${response.statusCode} - ${response.body}'); - } - - final data = jsonDecode(response.body); - return data['choices'][0]['message']['content'].trim(); // Adjust based on actual response structure - } - - -} \ No newline at end of file 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 deleted file mode 100644 index bbb6bdf9..00000000 --- a/team_a/teamA/lib/Api/llm/llm_api_modules_base.dart +++ /dev/null @@ -1,17 +0,0 @@ -// TODO: Put public facing types in this file. - -/// Checks if you are awesome. Spoiler: you are. -class Awesome { - bool get isAwesome => true; -} -//This is the abstract class for the LLM API module -//It has a single method called generate which takes a string prompt and returns a string -abstract class LLM { - final String apiKey; - String get url; - String get model; - - LLM(this.apiKey); - - Future generate(String prompt); -} diff --git a/team_a/teamA/lib/Api/llm/openai_api.dart b/team_a/teamA/lib/Api/llm/openai_api.dart deleted file mode 100644 index e5e96b1c..00000000 --- a/team_a/teamA/lib/Api/llm/openai_api.dart +++ /dev/null @@ -1,191 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:learninglens_app/Api/llm/llm_api_modules_base.dart'; -import 'package:learninglens_app/services/api_service.dart'; - -class OpenAiLLM implements LLM { - @override - final String apiKey; - @override - final String url = 'https://api.openai.com/v1/chat/completions'; - @override - final String model = 'gpt-4o-mini'; - OpenAiLLM(this.apiKey); - - Map convertHttpRespToJson(String httpResponseString) { - return (json.decode(httpResponseString) as Map); - } - - /// - /// - /// - String getPostBody(String queryMessage) { - return jsonEncode({ - 'model': model, - 'messages': [ - {'role': 'system', 'content': 'Be precise and concise'}, - {'role': 'user', 'content': queryMessage} - ] - }); - } - - /// - /// - /// - Map getPostHeaders() { - return ({ - 'accept': 'application/json', - 'content-type': 'application/json', - 'authorization': 'Bearer $apiKey', - }); - } - - /// - /// - /// - Uri getPostUrl() => Uri.https('api.openai.com', '/v1/chat/completions'); - - /// - /// - /// - Future postMessage( - Uri url, Map postHeaders, Object postBody) async { - final httpPackageResponse = - await ApiService().httpPost(url, headers: postHeaders, body: postBody); - - print(url); - print(postHeaders); - print(postBody); - - if (httpPackageResponse.statusCode != 200) { - print('Failed to retrieve the http package!'); - print('statusCode : ${httpPackageResponse.statusCode}'); - print('Reason: ${httpPackageResponse.body}'); - - return ""; - } - - print("In postmessage : ${httpPackageResponse.body}"); - return httpPackageResponse.body; - } - - List parseQueryResponse(String resp) { - // ignore: prefer_adjacent_string_concatenation - String quizRegExp = - // r'(<\?xml.*?\?>\s*(\s*.*?\s*.*?\s*(.*?)\s*.*?(\s*.*?)+\s*\s*.*?\s*(.*?)\s*.*?)+\s*)'; - r'(<\?xml.*?\?>\s*.*?)'; - - RegExp exp = RegExp(quizRegExp); - String respNoNewlines = resp.replaceAll('\n', ''); - Iterable matches = exp.allMatches(respNoNewlines); - List parsedResp = []; - - print("Parsing the query response - matches: $matches"); - - for (final m in matches) { - if (m.group(0) != null) { - parsedResp.add(m.group(0)!); - - print("This is a match : ${m.group(0)}"); - print("Number of groups in the match: ${m.groupCount}"); - print("parsedResp : $parsedResp"); - } - } - - return parsedResp; - } - - /// - /// - /// - Future postToLlm(String queryPrompt) async { - var resp = ""; - - // use the following test query so Perplexity doesn't charge - // 'How many stars are there in our galaxy?' - if (queryPrompt.isNotEmpty) { - resp = await queryAI(queryPrompt); - } - return resp; - } - - /// - /// - /// - Future queryAI(String query) async { - final postHeaders = getPostHeaders(); - final postBody = getPostBody(query); - final httpPackageUrl = getPostUrl(); - - final httpPackageRespString = - await postMessage(httpPackageUrl, postHeaders, postBody); - - final httpPackageResponseJson = - convertHttpRespToJson(httpPackageRespString); - - var retResponse = ""; - for (var respChoice in httpPackageResponseJson['choices']) { - retResponse += respChoice['message']['content']; - } - print("In queryAI - content : $retResponse"); - return retResponse; - } - - 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); - - // Check for successful response - if (response.statusCode == 200) { - var data = jsonDecode(response.body); - return data['choices'][0]['message']['content'] - .trim(); // Return the chat response - } else { - // Log the error response and handle failure cases - print('Failed to fetch response. Status code: ${response.statusCode}'); - print('Response body: ${response.body}'); - return 'Sorry, I couldn’t fetch a response. Please try again.'; - } - } catch (error) { - // Log and handle connection or parsing errors - print('Error occurred: $error'); - return 'An error occurred. Please check your internet connection and try again.'; - } - } - - @override - Future generate(String prompt) async { - print("In generate - prompt : $prompt"); - -final url = Uri.parse(this.url); - final headers = { - 'Authorization': 'Bearer $apiKey', - 'Content-Type': 'application/json', - }; - final body = jsonEncode({ - 'model': model, - 'messages': [ - {'role': 'system', 'content': 'You are a helpful assistant.'}, - {'role': 'user', 'content': prompt}, - ], - 'max_tokens': 500, // Limit response length - }); - - final response = await http.post(url, headers: headers, body: body); - if (response.statusCode != 200) { - throw Exception('OpenAI API error: ${response.statusCode} - ${response.body}'); - } - - final data = jsonDecode(response.body); - return data['choices'][0]['message']['content'].trim(); - - } - -} \ No newline at end of file diff --git a/team_a/teamA/lib/Api/llm/perplexity_api.dart b/team_a/teamA/lib/Api/llm/perplexity_api.dart deleted file mode 100644 index 49aae50a..00000000 --- a/team_a/teamA/lib/Api/llm/perplexity_api.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'dart:async'; -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 -{ - @override - final String apiKey; - @override - final String url = 'https://api.perplexity.ai/chat/completions'; - @override - final String model = 'llama-3.1-sonar-large-128k-online'; - - PerplexityLLM(this.apiKey); - - Map convertHttpRespToJson(String httpResponseString) - { - return (json.decode(httpResponseString) as Map); - } - - // - String getPostBody(String queryMessage) - { - return jsonEncode({ - // 'model': 'llama-3-sonar-large-32k-online', - //'model': 'llama-3.1-sonar-large-128k-chat', - 'model': model, - 'messages': [ - {'role': 'system', 'content': 'Be precise and concise'}, - {'content': queryMessage, 'role': 'user'} - ] - }); - } - - // - Map getPostHeaders() - { - return ({ - 'accept': 'application/json', - 'content-type': 'application/json', - 'authorization': 'Bearer $apiKey', - }); - } - - // - Uri getPostUrl() => Uri.https(this.url); - - // - Future postMessage( - Uri url, Map postHeaders, Object postBody) async - { - final httpPackageResponse = - await ApiService().httpPost(url, headers: postHeaders, body: postBody); - - if (httpPackageResponse.statusCode != 200) { - print('Failed to retrieve the http package!'); - print('statusCode : ${httpPackageResponse.statusCode}'); - print('body: ${httpPackageResponse.body}'); - return ""; - } - - print("In postmessage : ${httpPackageResponse.body}"); - return httpPackageResponse.body; - } - - List parseQueryResponse(String resp) - { - // ignore: prefer_adjacent_string_concatenation - String quizRegExp = - // r'(<\?xml.*?\?>\s*(\s*.*?\s*.*?\s*(.*?)\s*.*?(\s*.*?)+\s*\s*.*?\s*(.*?)\s*.*?)+\s*)'; - r'(<\?xml.*?\?>\s*.*?)'; - - RegExp exp = RegExp(quizRegExp); - String respNoNewlines = resp.replaceAll('\n', ''); - Iterable matches = exp.allMatches(respNoNewlines); - List parsedResp = []; - - print("Parsing the query response - matches: $matches"); - - for (final m in matches) { - if (m.group(0) != null) { - parsedResp.add(m.group(0)!); - - print("This is a match : ${m.group(0)}"); - print("Number of groups in the match: ${m.groupCount}"); - print("parsedResp : $parsedResp"); - } - } - - return parsedResp; - } - - // - Future postToLlm(String queryPrompt) async - { - var resp = ""; - - // use the following test query so Perplexity doesn't charge - // 'How many stars are there in our galaxy?' - if (queryPrompt.isNotEmpty) { - resp = await queryAI(queryPrompt); - } - return resp; - } - - // - Future queryAI(String query) async - { - final postHeaders = getPostHeaders(); - final postBody = getPostBody(query); - final httpPackageUrl = getPostUrl(); - - final httpPackageRespString = - await postMessage(httpPackageUrl, postHeaders, postBody); - - final httpPackageResponseJson = - convertHttpRespToJson(httpPackageRespString); - - var retResponse = ""; - for (var respChoice in httpPackageResponseJson['choices']) { - retResponse += respChoice['message']['content']; - } - // print("In queryAI - content : $retResponse"); - return retResponse; - } - - 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); - - // Check for successful response - if (response.statusCode == 200) { - var data = jsonDecode(response.body); - return data['choices'][0]['message']['content'] - .trim(); // Return the chat response - } else { - // Log the error response and handle failure cases - print('Failed to fetch response. Status code: ${response.statusCode}'); - print('Response body: ${response.body}'); - return 'Sorry, I couldn’t fetch a response. Please try again.'; - } - } catch (error) { - // Log and handle connection or parsing errors - print('Error occurred: $error'); - 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 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/llm/prompt_engine.dart b/team_a/teamA/lib/Api/llm/prompt_engine.dart deleted file mode 100644 index 95d338af..00000000 --- a/team_a/teamA/lib/Api/llm/prompt_engine.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'package:learninglens_app/beans/assignment_form.dart'; - -class PromptEngine { - - static const prompt_quizgen_college = - 'Generate a quiz in XML format ' - 'that is compatible with Moodle XML import. The quiz is to be on the subject of ' - '[subject] and should be related to [topic]. ' - 'The quiz should be the same level of difficulty for college [gradelevel] students of the ' - 'English-speaking language. '; - - static const prompt_quizgen_high_school = - 'Generate a quiz in XML format ' - 'that is compatible with Moodle XML import. The quiz is to be on the subject of ' - '[subject] and should be related to [topic]. ' - 'The quiz should be the same level of difficulty for high school [gradelevel] students of the ' - 'English-speaking language. '; - - static const prompt_quizgen_other = - 'Generate a quiz in XML format ' - 'that is compatible with Moodle XML import. The quiz is to be on the subject of ' - '[subject] and should be related to [topic]. ' - 'The quiz should be the same level of difficulty for [gradelevel] grade students of the ' - 'English-speaking language. '; - - - static const prompt_quizgen_xmlonly = - 'Provide only the XML in your response. '; - - static const prompt_quizgen_choice_example = - 'Please use this XML sample as a template for your response: Question 1 What does CPU stand for? Central Processing Unit Correct! The CPU is the primary component of a computer that processes instructions. Computer Processing Unit Incorrect. While close, this is not the correct term. Central Program Unit Incorrect. This is not the correct term for CPU. Control Processing Unit Incorrect. While the CPU does involve processing, this is not the correct term. Question 2 Which of the following is not a programming paradigm? Object-Oriented Programming Incorrect. Object-Oriented Programming is a valid programming paradigm. Functional Programming Incorrect. Functional Programming is a valid programming paradigm. Quantum Programming Correct! While quantum computing exists, "Quantum Programming" is not a standard programming paradigm. Procedural Programming Incorrect. Procedural Programming is a valid programming paradigm. Question 3 What is the primary function of an operating system? Manage computer hardware and software resources Correct! The operating system acts as an intermediary between programs and the computer hardware. Create documents and spreadsheets Incorrect. This is the function of application software, not the operating system. Browse the internet Incorrect. Web browsing is a function of web browser applications, not the operating system itself. Play video games Incorrect. Playing games is a function of game software, not the primary function of an operating system. Question 4 Which of the following is an example of a high-level programming language? Assembly Incorrect. Assembly is a low-level programming language. Machine Code Incorrect. Machine code is the lowest-level programming language. Python Correct! Python is an example of a high-level programming language. Binary Incorrect. Binary is not a programming language, but a number system used in computing. Question 5 What does HTML stand for? Hypertext Markup Language Correct! HTML is used to structure content on the web. Hyperlinks and Text Markup Language Incorrect. While HTML does involve hyperlinks, this is not the correct expansion of the acronym. Home Tool Markup Language Incorrect. This is not what HTML stands for. Hyper Transfer Markup Language Incorrect. This is not the correct expansion of HTML. '; - static const prompt_quizgen_truefalse_example = - 'Please use this XML sample as a template for your response: World War 2 Question 1The Normandy landings, also known as D-Day, took place on June 6, 1944.TrueCorrect! The Normandy landings, codenamed Operation Neptune, indeed took place on June 6, 1944, marking the beginning of the Allied invasion of Nazi-occupied Western Europe.FalseIncorrect. The Normandy landings, also known as D-Day, did occur on June 6, 1944. This date marks a crucial turning point in World War 2.World War 2 Question 2The atomic bombs dropped on Hiroshima and Nagasaki were codenamed "Little Boy" and "Fat Man" respectively.TrueCorrect! "Little Boy" was the codename for the bomb dropped on Hiroshima on August 6, 1945, while "Fat Man" was dropped on Nagasaki on August 9, 1945.FalseIncorrect. The atomic bombs were indeed codenamed "Little Boy" (Hiroshima) and "Fat Man" (Nagasaki). These were the only two nuclear weapons ever used in warfare.World War 2 Question 3The Battle of Stalingrad ended with a decisive Soviet victory, marking a turning point on the Eastern Front.TrueCorrect! The Battle of Stalingrad, which lasted from August 23, 1942, to February 2, 1943, ended in a decisive Soviet victory and is considered a major turning point in World War 2.FalseIncorrect. The Battle of Stalingrad did indeed end with a decisive Soviet victory, which marked a crucial turning point on the Eastern Front and in World War 2 as a whole. '; - static const prompt_quizgen_shortanswer_example = - 'Please use this XML sample as a template for your response: Combinatorics Question 1In how many ways can 5 distinct books be arranged on a shelf?1205!Combinatorics Question 2How many different 4-digit numbers can be formed using the digits 1, 2, 3, 4, 5, 6 if no digit can be repeated?360 '; - static const prompt_quizgen_essay_example = - 'Please use this XML sample as a template for your response: American Revolution Essay Write a well-structured essay analyzing the key factors that led to the American Revolution and its long-term impact on the formation of the United States. Your essay should address the following points:

  • Discuss at least three major events or policies that contributed to growing tensions between the American colonies and Great Britain in the decade leading up to 1776.
  • Explain the significance of the Declaration of Independence, both as a philosophical document and as a catalyst for revolutionary action.
  • Analyze the role of key figures such as George Washington, Thomas Jefferson, and Benjamin Franklin in shaping the course of the revolution.
  • Evaluate the impact of the American Revolution on the political, social, and economic structures of the newly formed United States.
  • Consider how the ideals of the American Revolution influenced later democratic movements both in the United States and around the world.

Your essay should be approximately 1000-1200 words in length. Use specific historical examples to support your arguments, and be sure to cite any sources you use.

]]>
This essay requires you to demonstrate your understanding of the causes and consequences of the American Revolution, as well as your ability to analyze historical events and their significance. 100 0 0 editor 1 15 0 0 Grading Criteria:

  • Clear thesis statement and well-structured argument: 20 points
  • Accurate discussion of pre-revolutionary events and policies: 20 points
  • Insightful analysis of the Declaration of Independence: 15 points
  • Thoughtful examination of key revolutionary figures: 15 points
  • Comprehensive evaluation of the revolution\'s impact: 20 points
  • Consideration of the revolution\'s broader influence: 10 points
  • Use of specific historical examples and proper citations: 10 points
]]>
'; - static const prompt_quizgen_coding_example = - 'Please use this XML sample as a template for your response: Dart List Manipulation: Filtering Even Numbers Write a Dart function called filterEvenNumbers that takes a list of integers as input and returns a new list containing only the even numbers from the input list. Your function should use the following signature:

 List filterEvenNumbers(List numbers)         

Requirements:

  • Use Dart\'s list methods to implement the filtering logic.
  • The original list should not be modified.
  • If the input list is empty, return an empty list.
  • Include comments to explain your code.

Test your function with different input lists to ensure it works correctly.

]]>
100 0 0 editor 1 30 0 0 void main() { List numbers1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; List result1 = filterEvenNumbers(numbers1); print(\'Test 1: \$result1\'); // Expected: [2, 4, 6, 8, 10] List numbers2 = [1, 3, 5, 7, 9]; List result2 = filterEvenNumbers(numbers2); print(\'Test 2: \$result2\'); // Expected: [] List numbers3 = []; List result3 = filterEvenNumbers(numbers3); print(\'Test 3: \$result3\'); // Expected: [] } List filterEvenNumbers(List numbers) { // Your implementation here } ]]> filterEvenNumbers(List numbers) { // Your code here } void main() { // Test your function here } ]]>
'; - // 'Please use this XML sample as a template for your response: Maximum grade goes here00editorfilepicker11501Level 4, the highest criteria score goes here.Level 3 criteria score goes hereLevel 2 criteria score goes hereLevel 1, the lowest criteria score goes hereInput goes hereExpected output goes here '; - // 'Please use this XML sample as a template for your response: Recursive Functions in DartImplement a recursive function in Dart that calculates the nth Fibonacci number. The Fibonacci sequence is defined as follows: the first two numbers are 0 and 1, and each subsequent number is the sum of the two preceding ones. Your function should take an integer n as input and return the nth Fibonacci number. Additionally, explain the time complexity of your recursive solution and suggest how you might optimize it using memoization. Provide your code implementation and explanation below.A well-implemented recursive Fibonacci function should correctly calculate the nth Fibonacci number. The explanation should discuss the exponential time complexity of the naive recursive solution and how memoization can improve it to linear time complexity.1000editor1150'; - - static const prompt_assistant = """ - You are EduLense, a specialized e-learning assistant. You have these possible functions: - 1) getUserCourses() - 2) getCourseParticipants(courseId=?) - 3) getQuizzes(courseID=?, quizTopicId=?) - 4) getQuizGradesForParticipants(courseId=?, quizId=?) - - If you need to call a function to fulfill the user's request, - respond EXACTLY in the format (on a single line): - CALL functionName(arg1=value1, arg2=value2) - - You can make multiple function calls if you need step-by-step data: - - For example, if the user says "What quizzes do I have for my Math course?", - you might first call: - CALL getUserCourses() - to find the course ID for "Math" (like 101). - - Then parse that result. If you see "Math 101 (ID: 101, quizTopicId: 201)", - call: - CALL getQuizzes(courseID=101, quizTopicId=) - - Once you have the quizzes, produce a final text answer (like "You have a midterm quiz..."). - - Avoid calling getUserCourses() repeatedly if you already have the data. - - If you do not need any function call, just provide a direct answer in plain text. - - Example conversation: - User: "Who is in my Math course?" - You might do: - CALL getUserCourses() - (assume the result shows "Math 101" => ID=101) - Then: - CALL getCourseParticipants(courseId=101) - Finally, once you get the participants, answer in plain text. - - Always provide a final text summary once you have enough data from function calls. - Do not keep calling functions indefinitely. - """; - - static String _FormatQuestionNumbers(AssignmentForm form) { - String ret = 'The quiz should have '; - List questionsCount = []; - int count; - if (form.multipleChoiceCount > 0) { - count = form.multipleChoiceCount; - print(count); - if (count == 1) { - questionsCount.add("$count multiple choice question"); - } else { - questionsCount.add("$count multiple choice questions"); - } - } if (form.trueFalseCount > 0) { - count = form.trueFalseCount; - if (count == 1) { - questionsCount.add("$count true or false question"); - } else { - questionsCount.add("$count true or false questions"); - } - } - if (form.shortAnswerCount > 0) { - count = form.shortAnswerCount; - if (count == 1) { - questionsCount.add("$count short answer question"); - } else { - questionsCount.add("$count short answer questions"); - } - } - return "$ret${questionsCount.join(", ")}. "; - } - - static String generatePrompt(AssignmentForm form) { - String prompt = prompt_quizgen_other + _FormatQuestionNumbers(form) + prompt_quizgen_choice_example; - prompt = prompt - .replaceAll('[subject]', form.subject) - .replaceAll('[topic]', form.topic) - .replaceAll('[gradelevel]', form.gradeLevel) - .replaceAll('[maxgrade]', form.maximumGrade.toString()); - - if (form.assignmentCount != null) { - prompt = prompt.replaceAll( - '[numassignments]', form.assignmentCount.toString()); - } - if (form.gradingCriteria != null) { - prompt = prompt.replaceAll('[rubriccriteria]', form.gradingCriteria!); - } - if (form.codingLanguage != null) { - prompt = prompt.replaceAll('[codinglanguage]', form.codingLanguage!); - } - prompt += prompt_quizgen_xmlonly; - return prompt; - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/Api/lms/constants/learning_lens.constants.dart b/team_a/teamA/lib/Api/lms/constants/learning_lens.constants.dart deleted file mode 100644 index 82453959..00000000 --- a/team_a/teamA/lib/Api/lms/constants/learning_lens.constants.dart +++ /dev/null @@ -1,4 +0,0 @@ -class LearningLensConstants{ - static const List gradeLevels = ['Kindergarten', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']; - -} \ No newline at end of file diff --git a/team_a/teamA/lib/Api/lms/enum/assignee_mode_enum.dart b/team_a/teamA/lib/Api/lms/enum/assignee_mode_enum.dart deleted file mode 100644 index 4cf9e307..00000000 --- a/team_a/teamA/lib/Api/lms/enum/assignee_mode_enum.dart +++ /dev/null @@ -1,6 +0,0 @@ -//Enum for AssigneeMode -enum AssigneeMode { - ASSIGNEE_MODE_UNSPECIFIED, - ALL_STUDENTS, - INDIVIDUAL_STUDENTS -} \ No newline at end of file diff --git a/team_a/teamA/lib/Api/lms/enum/course_state_enum.dart b/team_a/teamA/lib/Api/lms/enum/course_state_enum.dart deleted file mode 100644 index d1da73e6..00000000 --- a/team_a/teamA/lib/Api/lms/enum/course_state_enum.dart +++ /dev/null @@ -1,8 +0,0 @@ -//Enum for CourseState -enum CourseState { - COURSE_STATE_UNSPECIFIED, - ACTIVE, - ARCHIVED, - PROVISIONED, - DECLINED -} \ No newline at end of file diff --git a/team_a/teamA/lib/Api/lms/enum/course_work_state_enum.dart b/team_a/teamA/lib/Api/lms/enum/course_work_state_enum.dart deleted file mode 100644 index f6413959..00000000 --- a/team_a/teamA/lib/Api/lms/enum/course_work_state_enum.dart +++ /dev/null @@ -1,7 +0,0 @@ -//Enum for CourseWorkState -enum CourseWorkState { - COURSE_WORK_STATE_UNSPECIFIED, - PUBLISHED, - DRAFT, - DELETED -} \ No newline at end of file diff --git a/team_a/teamA/lib/Api/lms/enum/course_work_type_enum.dart b/team_a/teamA/lib/Api/lms/enum/course_work_type_enum.dart deleted file mode 100644 index 0e6c8eee..00000000 --- a/team_a/teamA/lib/Api/lms/enum/course_work_type_enum.dart +++ /dev/null @@ -1,7 +0,0 @@ -//Enum for CourseWorkType -enum CourseWorkType { - COURSE_WORK_TYPE_UNSPECIFIED, - ASSIGNMENT, - SHORT_ANSWER_QUESTION, - MULTIPLE_CHOICE_QUESTION -} \ No newline at end of file diff --git a/team_a/teamA/lib/Api/lms/enum/lms_enum.dart b/team_a/teamA/lib/Api/lms/enum/lms_enum.dart deleted file mode 100644 index 35de018f..00000000 --- a/team_a/teamA/lib/Api/lms/enum/lms_enum.dart +++ /dev/null @@ -1,4 +0,0 @@ -enum LmsType { - MOODLE, - GOOGLE, -} \ No newline at end of file diff --git a/team_a/teamA/lib/Api/lms/enum/preview_version_enum.dart b/team_a/teamA/lib/Api/lms/enum/preview_version_enum.dart deleted file mode 100644 index f09549a1..00000000 --- a/team_a/teamA/lib/Api/lms/enum/preview_version_enum.dart +++ /dev/null @@ -1,5 +0,0 @@ -//Enum for PreviewVersion -enum PreviewVersion { - PREVIEW_VERSION_UNSPECIFIED, - PUBLISHED_VERSION -} diff --git a/team_a/teamA/lib/Api/lms/enum/submission_mod_mode_enum.dart b/team_a/teamA/lib/Api/lms/enum/submission_mod_mode_enum.dart deleted file mode 100644 index aaebce84..00000000 --- a/team_a/teamA/lib/Api/lms/enum/submission_mod_mode_enum.dart +++ /dev/null @@ -1,6 +0,0 @@ -//Enum for SubmissionModificationMode -enum SubmissionModificationMode { - SUBMISSION_MODIFICATION_MODE_UNSPECIFIED, - MODIFIABLE_UNTIL_TURNED_IN, - MODIFIABLE_AFTER_TURNED_IN -} \ No newline at end of file diff --git a/team_a/teamA/lib/Api/lms/factory/lms_factory.dart b/team_a/teamA/lib/Api/lms/factory/lms_factory.dart deleted file mode 100644 index 128da031..00000000 --- a/team_a/teamA/lib/Api/lms/factory/lms_factory.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:learninglens_app/Api/lms/enum/lms_enum.dart'; -import 'package:learninglens_app/Api/lms/lms_interface.dart'; -import 'package:learninglens_app/Api/lms/moodle/moodle_lms_service.dart'; -import 'package:learninglens_app/Api/lms/google_classroom/google_lms_service.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; - -class LmsFactory { - - static MoodleLmsService _lmsServiceMoodle = MoodleLmsService(); - static GoogleLmsService _lmsServiceGoogle = GoogleLmsService(); - - static LmsInterface getLmsService() { - LmsType lmsType = LocalStorageService.getSelectedClassroom(); - - switch (lmsType) { - // TODO: Do we need this or can we just return Moodle as a default with the default: case? - // case LmsType.MOODLE: - // return getLmsServiceMoodle(); - case LmsType.GOOGLE: - return getLmsServiceGoogle(); - default: - // print('LMS type not found, defaulting to Moodle'); - return getLmsServiceMoodle(); - } - } - - static MoodleLmsService getLmsServiceMoodle() { - return _lmsServiceMoodle; - } - - static GoogleLmsService getLmsServiceGoogle() { - return _lmsServiceGoogle; - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/Api/lms/google_classroom/google_classroom_api.dart b/team_a/teamA/lib/Api/lms/google_classroom/google_classroom_api.dart deleted file mode 100644 index a4440515..00000000 --- a/team_a/teamA/lib/Api/lms/google_classroom/google_classroom_api.dart +++ /dev/null @@ -1,747 +0,0 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:learninglens_app/beans/lesson_plan.dart'; -import 'package:logging/logging.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; -class GoogleClassroomApi { - // final MainController _controller = MainController(); ***** Not used ***** - - Future _getToken() async { - final token = LocalStorageService.getGoogleAccessToken(); - if (token == null) { - print( - 'Error: No valid OAuth token. Ensure the required scopes are enabled. Token null'); - } - return token; - } - - // ----------------------------------------------------------------------- - // Creates a new Google Form - // ----------------------------------------------------------------------- - Future?> createForm(String? teacherFolderId, String title) async { - final token = await _getToken(); - if (token == null) return null; - - final url = Uri.parse('https://forms.googleapis.com/v1/forms'); - final headers = { - 'Authorization': 'Bearer $token', - 'Content-Type': 'application/json', - }; - - final body = jsonEncode({ - 'info': {'title': title, 'documentTitle': title}, - }); - - try { - final response = await http.post(url, headers: headers, body: body); - - print("Response Status: ${response.statusCode}"); - print("Response Body: ${response.body}"); - print('teacherFolderId: $teacherFolderId'); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - - // Move the folder to where GC expects it to be. - final driveUrl = Uri.https( - 'www.googleapis.com', - '/drive/v3/files/${data['formId']}', - { - 'addParents': teacherFolderId, - 'removeParents': 'root', - }, - ); - final driveHeaders = { - 'Authorization': 'Bearer $token', - 'Content-Type': 'application/json', - }; - final driveBody = jsonEncode({ - 'addParents': teacherFolderId, // New parent folder - 'removeParents': 'root', - }); - - // Remove from root (optional: fetch current parents if needed) - final driveResponse = await http.patch( - driveUrl, - headers: driveHeaders, - body: driveBody, - ); - print('***********************************************************************************'); - print("Drive Update Status: ${driveResponse.statusCode}"); - print("Drive Update Body: ${driveResponse.body}"); - - - return data; - } else { - print('Form creation failed: ${response.statusCode}'); - return null; - } - } catch (e) { - print('Error creating form: $e'); - return null; - } - } - - // ----------------------------------------------------------------------- - // Batches the update - // ----------------------------------------------------------------------- - Future?> batchUpdateForm( - String formId, List> requests) async { - final token = await _getToken(); - if (token == null) return null; - - final url = - Uri.parse('https://forms.googleapis.com/v1/forms/$formId:batchUpdate'); - final headers = { - 'Authorization': 'Bearer $token', - 'Content-Type': 'application/json', - }; - - final body = jsonEncode({ - 'requests': requests, - }); - - try { - final response = await http.post(url, headers: headers, body: body); - - print("Response Status: ${response.statusCode}"); - print("Response Body: ${response.body}"); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - return data; - } else { - print('Form batch update failed: ${response.statusCode}'); - return null; - } - } catch (e) { - print('Error updating form settings: $e'); - return null; - } - } - - //Future?> createAssignment(String courseid, String sectionid, String assignmentName, String startdate, String enddate, String rubricJson, String description); - // ----------------------------------------------------------------------- -// Creates a Classroom assignment and links the form -// ----------------------------------------------------------------------- - // Future createAssignment(String courseId, String title, - // String description, String responderUri, DateTime dueDate) async { - // //Change the date parameter to DateTime - // // Changed formId to responderUri in the parameters - // final token = await _getToken(); - // if (token == null) return null; - - // final url = Uri.parse( - // 'https://classroom.googleapis.com/v1/courses/$courseId/courseWork'); - // final headers = { - // 'Authorization': 'Bearer $token', - // 'Content-Type': 'application/json', - // }; - // final year = dueDate.year; - // final month = dueDate.month; - // final day = dueDate.day; - // final hours = dueDate.hour; - // final minutes = dueDate.minute; - - // final body = jsonEncode({ - // "title": title, - // "description": description, - // "workType": "ASSIGNMENT", - // "state": "PUBLISHED", - // "dueDate": {"year": year, "month": month, "day": day}, - // "dueTime": {"hours": hours, "minutes": minutes, "seconds": 0}, - // "materials": [ - // { - // "link": {"url": responderUri} - // } // Use the responderUri directly - THIS IS THE FIX! - // ] - // }); - - // try { - // final response = await http.post(url, headers: headers, body: body); - - // print("Response Status: ${response.statusCode}"); - // print("Response Body: ${response.body}"); - - // if (response.statusCode == 200) { - // final data = jsonDecode(response.body); - // final assignmentId = data['id']; - // print('Assignment created successfully with ID: $assignmentId'); - // return assignmentId; - // } else { - // print('Assignment creation failed: ${response.statusCode}'); - // return null; - // } - // } catch (e) { - // print('Error creating assignment: $e'); - // return null; - // } - // } - - Future createAssignment(String courseId, String title, - String description, String responderUri, String dueDate) async { - final log = Logger('GoogleClassroomApi creating Assignments'); - log.info('We are calling createAssignment from Google Classroom.'); - final token = await _getToken(); - if (token == null) return null; - - final url = Uri.parse( - 'https://classroom.googleapis.com/v1/courses/$courseId/courseWork'); - final headers = { - 'Authorization': 'Bearer $token', - 'Content-Type': 'application/json', - }; - - // Parse the dueDate string to extract year, month, day, hours, and minutes - List dateParts = dueDate.split('-'); - int year = int.parse(dateParts[0]); - int month = int.parse(dateParts[1]); - int day = int.parse(dateParts[2]); - int hours = int.parse(dateParts[3]); - int minutes = int.parse(dateParts[4]); - String? topicId = await getTopicId(courseId, 'Quiz') ?? '755868506953'; - - print('topic id is : $topicId'); - - //String? topicId ='755868506953'; - - - final body = jsonEncode({ - "title": title, - "description": description, - "topicId": topicId, - "workType": "ASSIGNMENT", - "state": "PUBLISHED", - "dueDate": {"year": year, "month": month, "day": day}, - "dueTime": {"hours": hours, "minutes": minutes, "seconds": 0}, - "materials": [ - { - "link": {"url": responderUri} - } // Use the responderUri directly - THIS IS THE FIX! - ] - }); - -print('body for creating assignment is : $body'); - try { - final response = await http.post(url, headers: headers, body: body); - - print("Response Status: ${response.statusCode}"); - print("Response Body: ${response.body}"); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - final assignmentId = data['id']; - print('Assignment created successfully with ID: $assignmentId'); - return assignmentId; - } else { - - print('Assignment creation failed inside createAssignment method : ${response.statusCode}'); - print(response.body); - return null; - } - } catch (e) { - print('Error creating assignment: $e'); - return null; - } - } - - Future getTopicIdByCreating(String courseId, String title) async { - final log = Logger('GoogleClassroomApi creating Topics'); - log.info('Calling createTopic from Google Classroom.'); - - // Obtain the OAuth2 token - final token = await _getToken(); - if (token == null) { - print('Failed to obtain token.'); - return null; - } - - final url = Uri.parse('https://classroom.googleapis.com/v1/courses/$courseId/topics'); - final headers = { - 'Authorization': 'Bearer $token', - 'Content-Type': 'application/json', - }; - final body = jsonEncode({ - "name": title, - }); - - try { - // Make the POST request to create the topic - final response = await http.post(url, headers: headers, body: body); - - if (response.statusCode == 200) { - // Parse the response and extract the topicId - final data = jsonDecode(response.body); - return data['topicId']; - } else { - print('Failed to create topic. Status code: ${response.statusCode}, Response: ${response.body}'); - } - } catch (e) { - print('Error creating topic: $e'); - } - - return null; -} -Future getTopicId(String courseId, String title) async { - final log = Logger('GoogleClassroomApi getting TopicId'); - log.info('Calling listTopics from Google Classroom.'); - - final token = await _getToken(); - if (token == null) { - print('Failed to obtain token.'); - return null; - } - - final url = Uri.parse('https://classroom.googleapis.com/v1/courses/$courseId/topics'); - final headers = { - 'Authorization': 'Bearer $token', - 'Accept': 'application/json', - }; - - try { - final response = await http.get(url, headers: headers); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - final topics = data['topic'] as List; - - for (var topic in topics) { - if (topic['name'] == title) { - return topic['topicId']; - } - } - - log.warning('Topic with name "$title" not found. So i need to create a new topic'); - return getTopicIdByCreating(courseId, title); - } else { - print('Failed to list topics. Status code: ${response.statusCode}, Response: ${response.body}'); - } - } catch (e) { - print('Error listing topics: $e'); - } - - return null; -} -/* - Future createCourseWorkMaterial(String courseId, String accessToken, String title, ) async { - final url = Uri.parse('https://classroom.googleapis.com/v1/courses/$courseId/courseWorkMaterials'); - - final headers = { - 'Authorization': 'Bearer $accessToken', - 'Content-Type': 'application/json', - }; - - final body = jsonEncode({ - "title": "My New Course Material", - "description": "This is a description of the material.", - "materials": [ - { - "link": { - "url": "https://example.com/material" - } - } - ], - "state": "PUBLISHED", - "scheduledTime": "2024-03-15T10:00:00Z", - "topicId": "your_topic_id" - }); - - final response = await http.post(url, headers: headers, body: body); - - if (response.statusCode == 200) { - // Success! The course work material was created. - print('Course work material created successfully!'); - print(response.body); // You might want to parse the response JSON here. - } else { - // Error! Something went wrong. - print('Error creating course work material: ${response.statusCode}'); - print(response.body); - } - } -*/ - // ----------------------------------------------------------------------- - // Course Work Realated Methods - // ----------------------------------------------------------------------- - - - Future createCourseWorkMaterial(String courseId, String title, String description, String materialUrl, - {String? topicId, DateTime? scheduledTime}) async { - print('Creating material: $title'); - - final accessToken = await _getToken(); - if (accessToken == null) return null; - - final googleClassroomApi = GoogleClassroomApi(); - String? topicID = await googleClassroomApi.getTopicId(courseId, "Lesson Plans"); - - final url = Uri.parse('https://classroom.googleapis.com/v1/courses/$courseId/courseWorkMaterials'); - final headers = {'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json'}; - final body = jsonEncode({ - "title": title, - "description": description, - "state": "PUBLISHED", - if (scheduledTime != null) "scheduledTime": scheduledTime.toUtc().toIso8601String(), - if (topicID != null) "topicId": topicID, - }); - - try { - final response = await http.post(url, headers: headers, body: body); - if (response.statusCode == 200) { - return jsonDecode(response.body)['id']; - } else { - print('Error creating material: ${response.statusCode}'); - return null; - } - } catch (e) { - print('Error creating material: $e'); - return null; - } - } - -/* -Future updateCourseWorkMaterial(String courseId, String materialId, String? title, String? description, - {String? topicId, DateTime? scheduledTime, String? state}) async { - print('Updating material: $materialId'); - - final accessToken = await _getToken(); - if (accessToken == null) return; - - final url = Uri.parse('https://classroom.googleapis.com/v1/courses/$courseId/courseWorkMaterials/$materialId'); - print('Update URL: $url'); - - final updateMask = _generateUpdateMask(title, description, topicId, scheduledTime, state); - final headers = { - 'Authorization': 'Bearer $accessToken', - 'Content-Type': 'application/json', - 'updateMask': updateMask, - }; - print('Update Mask: $updateMask'); - - final Map requestBody = {}; - if (title != null) requestBody["title"] = title; - if (description != null) requestBody["description"] = description; - // if (state != null) requestBody["state"] = state; - // if (scheduledTime != null) requestBody["scheduledTime"] = scheduledTime.toUtc().toIso8601String(); - // if (topicId != null) requestBody["topicId"] = topicId; - - final body = jsonEncode(requestBody); - - print('Request Body:=> $body'); - - try { - print('Inside the try block'); - final response = await http.patch(url, headers: headers, body: body); - print('Response Status Code: ${response.statusCode}'); - print('Response Body: ${response.body}'); - - if (response.statusCode == 200) { - getLessonPlan(courseId); - } else { - print('Error updating material: ${response.statusCode}'); - } - } catch (e) { - print('Error updating material: $e'); - } -} -*/ -Future updateCourseWorkMaterial( - String courseId, - String materialId, - String? title, - String? description, { - String? topicId, - DateTime? scheduledTime, - String? state, - }) async { - print('Updating material: $materialId'); - - final accessToken = await _getToken(); - if (accessToken == null) { - print('No valid access token found.'); - return; - } - - // Generate the updateMask - final updateMask = _generateUpdateMask(title, description, topicId, scheduledTime, state); - final url = Uri.parse( - 'https://classroom.googleapis.com/v1/courses/$courseId/courseWorkMaterials/$materialId?updateMask=$updateMask'); - print('Update URL: $url'); - - final headers = { - 'Authorization': 'Bearer $accessToken', - 'Content-Type': 'application/json', - }; - - final Map requestBody = {}; - if (title != null) requestBody["title"] = title; - if (description != null) requestBody["description"] = description; - // Uncomment and use these if needed - // if (state != null) requestBody["state"] = state; - // if (scheduledTime != null) requestBody["scheduledTime"] = scheduledTime.toUtc().toIso8601String(); - // if (topicId != null) requestBody["topicId"] = topicId; - - final body = jsonEncode(requestBody); - print('Request Body: $body'); - - final client = http.Client(); - try { - final response = await client.patch(url, headers: headers, body: body); - print('Response Status Code: ${response.statusCode}'); - print('Response Body: ${response.body}'); - - if (response.statusCode == 200) { - print('Material updated successfully.'); - // Note: We’ll refresh the UI in the calling code, not here - } else { - print('Error updating material: ${response.statusCode} - ${response.body}'); - } - } catch (e) { - print('Detailed error updating material: $e'); - } finally { - client.close(); - } -} - -// Helper method to generate updateMask (unchanged) -String _generateUpdateMask(String? title, String? description, String? topicId, DateTime? scheduledTime, String? state) { - List fields = []; - if (title != null) fields.add('title'); - if (description != null) fields.add('description'); - if (topicId != null) fields.add('topicId'); - if (scheduledTime != null) fields.add('scheduledTime'); - if (state != null) fields.add('state'); - return fields.join(','); -} - - Future deleteCourseWorkMaterial(String courseId, String materialId) async { - print('Deleting material: $materialId'); - final accessToken = await _getToken(); - if (accessToken == null) return; - - final url = Uri.parse('https://classroom.googleapis.com/v1/courses/$courseId/courseWorkMaterials/$materialId'); - final headers = {'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json'}; - - try { - final response = await http.delete(url, headers: headers); - - print(response.statusCode); - print(response.body); - - if (response.statusCode == 200 || response.statusCode == 204) { - getLessonPlan(courseId); - } else { - print('Error deleting material: ${response.statusCode}'); - } - } catch (e) { - print('Error deleting material: $e'); - } - } - - - -Future getLessonPlan(String courseId) async { - LessonPlan lessonPlan = LessonPlan.empty(); - try { - final accessToken = await _getToken(); - if (accessToken == null) return; - - final url = Uri.parse('https://classroom.googleapis.com/v1/courses/$courseId/courseWorkMaterials'); - final headers = {'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json'}; - final response = await http.get(url, headers: headers); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - - if (data.containsKey('courseWorkMaterial')) { - List lessonPlans = (data['courseWorkMaterial'] as List) - .map((item) => lessonPlan.fromGoogleJson(item as Map)) - .toList(); - - print('Lesson Plans: $lessonPlans'); - } else { - print('No coursework materials found.'); - } - } else { - print('Error loading coursework materials: ${response.statusCode}'); - } - } catch (e) { - print('Error loading coursework materials: $e'); - } -} - - -// ----------------------------------------------------------------------- -// retrive the quiz questions -// ----------------------------------------------------------------------- - Future?> getQuizQuestions(String courseId, String quizId) async { - final token = await _getToken(); - if (token == null) return null; - - final url = Uri.parse('https://classroom.googleapis.com/v1/courses/$courseId/courseWork/$quizId'); - final headers = { - 'Authorization': 'Bearer $token', - 'Content-Type': 'application/json', - }; - - try { - final response = await http.get(url, headers: headers); - - print("Response Status: ${response.statusCode}"); - print("Response Body: ${response.body}"); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - return data; - } else { - print('Quiz questions retrieval failed: ${response.statusCode}'); - return null; - } - } catch (e) { - print('Error retrieving quiz questions: $e'); - return null; - } - } - - // ----------------------------------------------------------------------- - // Updates the course work - // ----------------------------------------------------------------------- - Future?> updateCourseWork(String courseId, String courseWorkId, String title, String description, - String? dueDate, String? dueTime, String? rubricJson, String? state) async { - final token = await _getToken(); - if (token == null) return null; - - final url = Uri.parse('https://classroom.googleapis.com/v1/courses/$courseId/courseWork/$courseWorkId'); - final headers = { - 'Authorization': 'Bearer $token', - 'Content-Type': 'application/json', - }; - - final body = jsonEncode({ - 'title': title, - 'description': description, - 'dueDate': dueDate, - 'dueTime': dueTime, - 'rubric': rubricJson, - 'state': state, - }); - - try { - final response = await http.put(url, headers: headers, body: body); - - print("Response Status: ${response.statusCode}"); - print("Response Body: ${response.body}"); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - return data; - } else { - print('Course work update failed: ${response.statusCode}'); - return null; - } - } catch (e) { - print('Error updating course work: $e'); - return null; - } - } - - // ----------------------------------------------------------------------- - - // Method to retrieve Google Form questions from an assignment - // ----------------------------------------------------------------------- - - -Future> getAssignmentFormQuestions(String? courseId, String? courseWorkId) async { - // "courseId": "750797786103", - //"id": "757589683856", - try { - // Step 1: Retrieve the stored access token - final token = await _getToken(); - print('Token: $token'); // Debug output - - if (token == null) { - throw Exception('No access token found. Please log in again.'); - } - - // Step 2: Fetch assignment details from Google Classroom API - final courseworkUrl = - 'https://classroom.googleapis.com/v1/courses/$courseId/courseWork/$courseWorkId'; - final courseworkResponse = await http.get( - Uri.parse(courseworkUrl), - headers: { - 'Authorization': 'Bearer $token', - 'Content-Type': 'application/json', - }, - ); - - print('Coursework URL: $courseworkUrl'); // Debug output - print('Coursework Response: ${courseworkResponse.body}'); // Debug output - - if (courseworkResponse.statusCode != 200) { - throw Exception( - 'Failed to fetch coursework: ${courseworkResponse.statusCode} - ${courseworkResponse.body}'); - } - - final courseworkData = jsonDecode(courseworkResponse.body); - print('Coursework Data: $courseworkData'); // Debug output - - // Step 3: Extract Form URL from materials - String? formUrl; - if (courseworkData['materials'] != null && courseworkData['materials'].isNotEmpty) { - for (var material in courseworkData['materials']) { - if (material['link'] != null && material['link']['url'] != null) { - final url = material['link']['url']; - if (url.contains('docs.google.com/forms')) { - formUrl = url; - break; - } - } - } - } - - if (formUrl == null) { - throw Exception('No Google Form found in assignment materials.'); - } - print('Form URL: $formUrl'); // Debug output - - // Step 4: Extract Form ID from the URL - final formId = formUrl.split('/d/e/')[1].split('/')[0]; - print('Form ID: $formId'); // Debug output - - // Step 5: Fetch form details from Google Forms API - final formsUrl = 'https://forms.googleapis.com/v1/forms/$formId'; - final formsResponse = await http.get( - Uri.parse(formsUrl), - headers: { - 'Authorization': 'Bearer $token', - 'Content-Type': 'application/json', - }, - ); - - if (formsResponse.statusCode != 200) { - throw Exception( - 'Failed to fetch form: ${formsResponse.statusCode} - ${formsResponse.body}'); - } - - final formData = jsonDecode(formsResponse.body); - print('Form Data: $formData'); // Debug output - - // Step 6: Extract questions from the form - List questions = []; - if (formData['items'] != null) { - for (var item in formData['items']) { - if (item['questionItem'] != null && item['questionItem']['question'] != null) { - questions.add(item['questionItem']['question']['text']); - } - } - } - - return questions; - } catch (e) { - print('Error retrieving form questions: $e'); - return []; - } -} - - - -} diff --git a/team_a/teamA/lib/Api/lms/google_classroom/google_lms_service.dart b/team_a/teamA/lib/Api/lms/google_classroom/google_lms_service.dart deleted file mode 100644 index 2c74b7b1..00000000 --- a/team_a/teamA/lib/Api/lms/google_classroom/google_lms_service.dart +++ /dev/null @@ -1,1098 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:http/http.dart' as http; -import 'package:learninglens_app/beans/g_question_form_data.dart'; -import 'package:logger/logger.dart'; -import 'package:learninglens_app/beans/quiz_type.dart'; -import 'package:xml/xml.dart' as xml; -import 'package:google_sign_in/google_sign_in.dart'; -import 'package:learninglens_app/Api/lms/lms_interface.dart'; -import 'package:learninglens_app/beans/course.dart'; -import 'package:learninglens_app/beans/quiz.dart'; -import 'package:learninglens_app/beans/assignment.dart'; -import 'package:learninglens_app/beans/participant.dart'; -import 'package:learninglens_app/beans/submission_status.dart'; -import 'package:learninglens_app/beans/grade.dart'; -import 'package:learninglens_app/beans/submission.dart'; -import 'package:learninglens_app/beans/submission_with_grade.dart'; -import 'package:learninglens_app/beans/moodle_rubric.dart'; -import 'package:learninglens_app/services/api_service.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; -import 'package:learninglens_app/Api/lms/google_classroom/google_classroom_api.dart'; // Import the updated API - -/// A Singleton class for Moodle API access implementing [LmsInterface]. -class GoogleLmsService extends LmsInterface { - // Needed?? - final GoogleClassroomApi _classroomApi = GoogleClassroomApi(); - - // **************************************************************************************** - // Static / Singleton internals - // **************************************************************************************** - - static final GoogleLmsService _instance = GoogleLmsService._internal(); - - /// The singleton accessor. - factory GoogleLmsService() => _instance; - - /// Private named constructor. - GoogleLmsService._internal(); - - // **************************************************************************************** - // Fields implementing LmsInterface - // **************************************************************************************** - - @override - String serverUrl = ''; // The Google REST endpoint - - // The user token is kept private (not in the interface). - String? _userToken; - - @override - String apiURL = - 'https://classroom.googleapis.com/v1'; // Base URL for your Google Classroom - @override - String? userName; - @override - String? firstName; - @override - String? lastName; - @override - String? siteName = 'Google Classroom'; - @override - String? fullName; - @override - String? profileImage; - @override - List? courses; - - late GoogleSignIn _googleSignIn; - - // **************************************************************************************** - // Auth / Login - // **************************************************************************************** - - @override - Future login(String username, String password, String baseURL) { - // TODO: implement google api code - throw UnimplementedError(); - } - - Future loginOath(String clientID) async { - print('Logging in to Google Classroom...'); - - _googleSignIn = GoogleSignIn( - clientId: clientID, - scopes: [ - 'email', - 'profile', - 'https://www.googleapis.com/auth/classroom.courses', - 'https://www.googleapis.com/auth/classroom.topics', - 'https://www.googleapis.com/auth/classroom.rosters', - 'https://www.googleapis.com/auth/classroom.coursework.students', - 'https://www.googleapis.com/auth/classroom.coursework.me', - 'https://www.googleapis.com/auth/classroom.courses.readonly', - 'https://www.googleapis.com/auth/forms.body', - 'https://www.googleapis.com/auth/forms.responses.readonly', - 'https://www.googleapis.com/auth/classroom.courseworkmaterials.readonly', - 'https://www.googleapis.com/auth/classroom.courseworkmaterials', - 'https://www.googleapis.com/auth/forms.body.readonly', - 'https://www.googleapis.com/auth/drive.file', - ], - ); - - try { - final GoogleSignInAccount? googleUser = await _googleSignIn.signIn(); - if (googleUser == null) { - throw Exception("Google Sign-In was cancelled by the user."); - } - - // Get the user's name - userName = googleUser.email.split("@").first; - fullName = googleUser.displayName ?? "Unknown User"; - - List nameParts = fullName!.split(" "); - - firstName = nameParts.isNotEmpty ? nameParts.first : ""; - lastName = nameParts.length > 1 ? nameParts.sublist(1).join(" ") : ""; - - print('Welcome, ${firstName ?? 'User'}'); - - final GoogleSignInAuthentication googleAuth = - await googleUser.authentication; - _userToken = googleAuth.accessToken; - - if (_userToken == null) { - throw Exception("Failed to obtain access token."); - } - - LocalStorageService.saveGoogleAccessToken(_userToken!); - } catch (error) { - print("Google Sign-In Error: $error"); - throw Exception("Google Sign-In failed: $error"); - } - - courses = await getUserCourses(); - - // for (Course course in courses!) { - // print('teacherFolderId: ${course.teacherFolderId}'); - // } - } - - @override - bool isLoggedIn() { - return _userToken != null; - } - - String getGoogleAccessToken() { - return _userToken!; - } - - @override - Future isUserTeacher(List moodleCourses) async { - // TODO: implement google api code - throw UnimplementedError(); - } - - @override - void logout() { - print('Logging out of Google...'); - _googleSignIn.signOut(); - resetLMSUserInfo(); - } - - @override - void resetLMSUserInfo() { - // Clear all user-related fields - _userToken = null; - apiURL = ''; - userName = null; - firstName = null; - lastName = null; - siteName = null; - fullName = null; - profileImage = null; - courses = []; - } - - // **************************************************************************************** - // Course-related methods - // **************************************************************************************** - - @override - Future> getCourses() async { - // TODO: implement google api code - // Never called?? - throw UnimplementedError(); - } - - @override - Future> getUserCourses() async { - if (_userToken == null) throw StateError('User not logged in to Google'); - - final response = await ApiService().httpGet( - Uri.parse('https://classroom.googleapis.com/v1/courses'), - headers: {'Authorization': 'Bearer $_userToken'}, - ); - - // TODO: remove after testing. - // print('Google: ${response.body}'); - - if (response.statusCode != 200) { - throw HttpException(response.body); - } - - final decodedJson = jsonDecode(response.body); - List courses; - - // The response can be either a List or a Map with a 'courses' key - if (decodedJson is List) { - courses = - decodedJson.map((i) => Course.empty().fromGoogleJson(i)).toList(); - } else if (decodedJson is Map) { - final courseList = decodedJson['courses'] as List; - courses = - courseList.map((i) => Course.empty().fromGoogleJson(i)).toList(); - } else { - throw StateError('Unexpected response format from Moodle'); - } - - // Optionally fetch quizzes/essays for each course - for (Course course in courses) { - // set topic ids - final responseTopics = await ApiService().httpGet( - Uri.parse( - 'https://classroom.googleapis.com/v1/courses/${course.id}/topics/'), - headers: {'Authorization': 'Bearer $_userToken'}, - ); - - var decodedResponseTopics = jsonDecode(responseTopics.body); - - if (decodedResponseTopics.containsKey('topic')) { - List topics = decodedResponseTopics["topic"]; - - // Iterate and capture topicIds - for (var topic in topics) { - if (topic['name'] == 'Quiz') { - course.quizTopicId = int.parse(topic['topicId']); - // print('Id for quiz'); - // print(topic['topicId']); - } else if (topic['name'] == 'Essay') { - course.essayTopicId = int.parse(topic['topicId']); - // print('Id for essay'); - // print(topic['topicId']); - } - } - } - - course.quizzes = await getQuizzes(course.id, topicId: course.quizTopicId); - // print('Quizzes for course ${course.id}: ${course.quizzes}'); - course.essays = await getEssays(course.id, topicId: course.essayTopicId); - // print('Essays for course ${course.id}: ${course.essays}'); - } - - return courses; - } - - @override - Future> getCourseParticipants(String courseId) async { - if (_userToken == null) { - throw StateError('User not logged in to Google Classroom'); - } - - final List participants = []; - - // Fetch students - final studentsResponse = await ApiService().httpGet( - Uri.parse(apiURL + '/courses/$courseId/students'), - headers: {'Authorization': 'Bearer $_userToken'}, - ); - - if (studentsResponse.statusCode == 200) { - final studentsJson = jsonDecode(studentsResponse.body); - if (studentsJson.containsKey('students')) { - for (var student in studentsJson['students']) { - participants.add(Participant( - id: student['userId'] - .hashCode, // Google Classroom does not provide numeric IDs - fullname: student['profile']['name']['fullName'], - firstname: student['profile']['name']['givenName'], - lastname: student['profile']['name']['familyName'], - roles: ['student'], - )); - } - } - } else { - throw HttpException('Failed to fetch students: ${studentsResponse.body}'); - } - - // Fetch teachers - final teachersResponse = await ApiService().httpGet( - Uri.parse(apiURL + '/courses/$courseId/teachers'), - headers: {'Authorization': 'Bearer $_userToken'}, - ); - - if (teachersResponse.statusCode == 200) { - final teachersJson = jsonDecode(teachersResponse.body); - if (teachersJson.containsKey('teachers')) { - for (var teacher in teachersJson['teachers']) { - participants.add(Participant( - id: teacher['userId'].hashCode, - fullname: teacher['profile']['name']['fullName'], - firstname: teacher['profile']['name']['givenName'], - lastname: teacher['profile']['name']['familyName'], - roles: ['teacher'], - )); - } - } - } else { - throw HttpException('Failed to fetch teachers: ${teachersResponse.body}'); - } - - return participants; - } - - // **************************************************************************************** - // Quiz methods - // **************************************************************************************** - - @override - Future importQuiz(String courseid, String quizXml) async { - // TODO: implement google api code - throw UnimplementedError( - 'This feature is not supported by Google Classroom. Please contact the developer.'); - } - - @override - Future> getQuizzes(int? courseID, {int? topicId}) async { - if (_userToken == null) - throw StateError('User not logged in to Google Classroom'); - - final response = await ApiService().httpGet( - Uri.parse( - 'https://classroom.googleapis.com/v1/courses/$courseID/courseWork'), - headers: {'Authorization': 'Bearer $_userToken'}, - ); - - // print('quizlist: ${response.body}'); - - if (response.statusCode != 200) { - throw HttpException(response.body); - } - - // print('Getting quizzes from Google Classroom...'); - // print(response.body); - - final quizzesMap = jsonDecode(response.body) as Map; - final decodedJson = quizzesMap['courseWork'] as List?; - - if (decodedJson == null) { - return []; - } - - List quizList = []; - for (var item in decodedJson) { - // print('Item: $item'); - // If courseID is null, return all quizzes; otherwise filter by course - if (courseID == null || int.parse(item['courseId']) == courseID) { - if (topicId != null && item.containsKey('topicId')) { - if (int.parse(item['topicId']) == topicId) { - quizList.add(Quiz.fromGoogleJson(item)); - } - } - } - } - - // print('I am getting this quiz list: $quizList'); - - return quizList; - } - - @override - Future createQuiz(String courseid, String quizname, String quizintro, - String sectionid, String timeopen, String timeclose) async { - print('Creating quiz in Google Classroom...'); - print('Course ID: $courseid'); - print('Quiz Name: $quizname'); - print('Quiz Intro: $quizintro'); - print('Section ID: $sectionid'); - print('Time Open: $timeopen'); - print('Time Close: $timeclose'); - - try { - // Convert timeopen to ISO 8601 format - String formattedTimeOpen = DateTime.parse(timeopen).toIso8601String(); - - String? assignmentId = await createAssignmentHelper( - courseid, quizname, quizintro, sectionid, formattedTimeOpen); - - if (assignmentId != null) { - return int.parse(assignmentId); - } else { - print('Failed to create quiz'); - return null; - } - } catch (e) { - print('Error creating quiz: $e'); - return null; - } - } - - Future createAssignmentHelper(String courseId, String title, - String description, String responderUri, String dueDate) async { - print('Creating assignment in Google Classroom... Inside helper'); - print('Course ID: $courseId'); - print('Title: $title'); - print('Description: $description'); - print('Responder URI: $responderUri'); - print('Due Date: $dueDate'); - - final token = _userToken; - if (token == null) { - print('User token is null'); - return null; - } - - final url = Uri.parse( - 'https://classroom.googleapis.com/v1/courses/$courseId/courseWork'); - final headers = { - 'Authorization': 'Bearer $token', - 'Content-Type': 'application/json', - }; - - // Parse the dueDate string - DateTime parsedDate = DateTime.parse(dueDate); - - final body = jsonEncode({ - "title": title, - "description": description, - "workType": "ASSIGNMENT", - "state": "PUBLISHED", - "dueDate": { - "year": parsedDate.year, - "month": parsedDate.month, - "day": parsedDate.day - }, - "dueTime": { - "hours": parsedDate.hour, - "minutes": parsedDate.minute, - "seconds": 0 - }, - "materials": [ - { - "link": {"url": responderUri} - } - ] - }); - - // Print request details - print('Request URL: $url'); - print('Request Headers: $headers'); - print('Request Body: $body'); - - try { - final response = await http.post(url, headers: headers, body: body); - - print("Response Status: ${response.statusCode}"); - print("Response Body: ${response.body}"); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - final assignmentId = data['id']; - print('Assignment created successfully with ID: $assignmentId'); - return assignmentId; - } else { - print('Assignment creation failed: ${response.statusCode}'); - print('Error message: ${response.body}'); - return null; - } - } catch (e) { - print('Error creating assignment: $e'); - return null; - } - } - - @override - Future addRandomQuestions( - String categoryid, String quizid, String numquestions) async { - print('Adding random questions to quiz...'); - print('Category ID: $categoryid'); - // TODO: implement google api code - throw UnimplementedError( - 'This feature is not supported by Google Classroom. Please contact the developer.'); - } - - @override - Future importQuizQuestions(String courseid, String quizXml) async { - print('Importing quiz questions...'); - print('Course ID: $courseid'); - print('Quiz XML: $quizXml'); - // TODO: implement google api code - throw UnimplementedError( - 'This feature is not supported by Google Classroom. Please contact the developer.'); - } - - @override - Future> getQuestionsFromQuiz(int quizId) async { - // TODO: implement google api code - throw UnimplementedError( - 'This feature is not supported by Google Classroom. Please contact the developer.'); - } - - Future getAssignmentFormQuestions( - String coursedId, String courseWorkId) async { - print( - 'Fetching form questions for course $coursedId, coursework $courseWorkId...'); - try { - final accessToken = await _getToken(); - if (accessToken == null) { - throw Exception('No access token found. Please log in again.'); - } - - final courseworkUrl = - 'https://classroom.googleapis.com/v1/courses/$coursedId/courseWork/$courseWorkId'; - final courseworkResponse = await http.get( - Uri.parse(courseworkUrl), - headers: { - 'Authorization': 'Bearer $accessToken', - 'Content-Type': 'application/json', - }, - ); - - if (courseworkResponse.statusCode != 200) { - throw Exception( - 'Failed to fetch coursework: ${courseworkResponse.statusCode} - ${courseworkResponse.body}'); - } - - final courseworkData = jsonDecode(courseworkResponse.body); - print('Coursework data: $courseworkData'); - - String? formUrl; - if (courseworkData['materials'] != null && - courseworkData['materials'].isNotEmpty) { - for (var material in courseworkData['materials']) { - if (material['link'] != null && material['link']['url'] != null) { - final url = material['link']['url']; - if (url.contains('docs.google.com/forms')) { - formUrl = url; - break; - } - } - if (material['form'] != null && material['form']['formUrl'] != null) { - final url = material['form']['formUrl']; - if (url.contains('docs.google.com/forms')) { - formUrl = url; - break; - } - } - } - } - - if (formUrl == null) { - throw Exception('No Google Form found in assignment materials.'); - } - print('Extracted Form URL: $formUrl'); - - // Extract and format dates to YYYY-MM-DD - String? startDate; - if (courseworkData['creationTime'] != null) { - final dateTime = DateTime.parse(courseworkData['creationTime']); - startDate = - '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')}'; - } - - String? endDate; - if (courseworkData['dueDate'] != null) { - final dueDate = courseworkData['dueDate']; - endDate = - '${dueDate['year']}-${dueDate['month'].toString().padLeft(2, '0')}-${dueDate['day'].toString().padLeft(2, '0')}'; - } - - String? status = courseworkData['state']; - - final formId = await getFormIdFromViewformUrl(formUrl, accessToken); - if (formId == null) { - throw Exception('Failed to retrieve Form ID from viewform URL'); - } - print('Form ID: $formId'); - - final formsUrl = 'https://forms.googleapis.com/v1/forms/$formId'; - final formsResponse = await http.get( - Uri.parse(formsUrl), - headers: { - 'Authorization': 'Bearer $accessToken', - 'Content-Type': 'application/json', - }, - ); - - if (formsResponse.statusCode != 200) { - throw Exception( - 'Failed to fetch form: ${formsResponse.statusCode} - ${formsResponse.body}'); - } - - final formData = jsonDecode(formsResponse.body); - print('Form data: $formData'); - - String title = formData['info']?['title'] ?? 'Untitled Form'; - List questions = []; - if (formData['items'] != null) { - for (var item in formData['items']) { - if (item['questionItem'] != null && - item['questionItem']['question'] != null) { - String questionText = item['title'] ?? 'Unnamed Question'; - List options = []; - if (item['questionItem']['question']['choiceQuestion'] != null) { - var choiceQuestion = - item['questionItem']['question']['choiceQuestion']; - if (choiceQuestion['options'] != null) { - for (var option in choiceQuestion['options']) { - options.add(option['value'] ?? 'No Option'); - } - } - } - questions - .add(QuestionData(question: questionText, options: options)); - } - } - } - - return FormData( - title: title, - questions: questions, - startDate: startDate, - endDate: endDate, - formUrl: formUrl, - status: status, - ); - } catch (e) { - print('Error retrieving form questions: $e'); - return FormData(title: 'Error', questions: []); - } - } - - Future getFormIdFromViewformUrl( - String viewformUrl, String accessToken) async { - try { - final publicKey = viewformUrl.split('/d/e/')[1].split('/')[0]; - print('Public key: $publicKey'); - - final driveUrl = 'https://www.googleapis.com/drive/v3/files' - '?q=mimeType="application/vnd.google-apps.form"' - '&fields=files(id,name)' - '&spaces=drive'; - final driveResponse = await http.get( - Uri.parse(driveUrl), - headers: { - 'Authorization': 'Bearer $accessToken', - 'Content-Type': 'application/json', - }, - ); - - if (driveResponse.statusCode != 200) { - throw Exception('Failed to fetch Drive files: ${driveResponse.body}'); - } - - final driveData = jsonDecode(driveResponse.body); - print('Drive data: $driveData'); - - for (var file in driveData['files']) { - final fileId = file['id']; - print('Checking file ID: $fileId'); - - final formsUrl = 'https://forms.googleapis.com/v1/forms/$fileId'; - final formResponse = await http.get( - Uri.parse(formsUrl), - headers: { - 'Authorization': 'Bearer $accessToken', - 'Content-Type': 'application/json', - }, - ); - - if (formResponse.statusCode == 200) { - final formData = jsonDecode(formResponse.body); - final responderUri = formData['responderUri']; - print('Responder URI for $fileId: $responderUri'); - - if (responderUri != null && responderUri.contains(publicKey)) { - print('Matched form ID: $fileId'); - return fileId; - } - } else { - print('Failed to fetch form $fileId: ${formResponse.statusCode}'); - } - } - - throw Exception( - 'No matching form found in Drive for the provided viewform URL'); - } catch (e) { - print('Error fetching Form ID from Drive: $e'); - return null; - } - } - - Future _getToken() async { - final token = LocalStorageService.getGoogleAccessToken(); - if (token == null) { - print( - 'Error: No valid OAuth token. Ensure the required scopes are enabled. Token null'); - } - return token; - } - - // **************************************************************************************** - // Assignment methods - // **************************************************************************************** - - @override - Future> getEssays(int? courseID, {int? topicId}) async { - if (_userToken == null) - throw StateError('User not logged in to Google Classroom'); - - final response = await ApiService().httpGet( - Uri.parse( - 'https://classroom.googleapis.com/v1/courses/$courseID/courseWork'), - headers: {'Authorization': 'Bearer $_userToken'}, - ); - - if (response.statusCode != 200) { - throw HttpException(response.body); - } - - final essaysMap = jsonDecode(response.body) as Map; - final decodedJson = essaysMap['courseWork'] as List?; - - if (decodedJson == null) { - return []; - } - - List essayList = []; - for (var item in decodedJson) { - // If courseID is null, return all quizzes; otherwise filter by course - if (courseID == null || int.parse(item['courseId']) == courseID) { - if (topicId != null && item.containsKey('topicId')) { - if (int.parse(item['topicId']) == topicId) { - essayList.add(Assignment.empty().fromGoogleJson(item)); - } - } - } - } - - return essayList; - } - - @override - Future?> createAssignment( - String courseid, - String sectionid, - String assignmentName, - String startdate, - String enddate, - String rubricJson, - String description, - ) async { - print('Creating assignment...'); - print('Course ID: $courseid'); - print('Section ID: $sectionid'); - print('Assignment Name: $assignmentName'); - print('Start Date: $startdate'); - print('End Date: $enddate'); - // TODO: implement google api code - throw UnimplementedError(); - } - - @override - Future getContextId(int assignmentId, String courseId) async { - print('Getting context ID...'); - print('Assignment ID: $assignmentId'); - print('Course ID: $courseId'); - // TODO: implement google api code - throw UnimplementedError( - 'This feature is not supported by Google Classroom. Please contact the developer.'); - } - - // **************************************************************************************** - // Submissions and grading - // **************************************************************************************** - - @override - Future> getAssignmentSubmissions(int assignmentId) async { - print('Getting assignment submissions...'); - print('Assignment ID: $assignmentId'); - // TODO: implement google api code - throw UnimplementedError( - 'This feature is not supported by Google Classroom. Please contact the developer.'); - } - - @override - Future> getSubmissionsWithGrades( - int assignmentId) async { - print('Getting submissions with grades...'); - print('Assignment ID: $assignmentId'); - - // TODO: implement google api code - throw UnimplementedError( - 'This feature is not supported by Google Classroom. Please contact the developer.'); - } - - @override - Future getSubmissionStatus( - int assignmentId, int userId) async { - print('Getting submission status...'); - print('Assignment ID: $assignmentId'); - print('User ID: $userId'); - // TODO: implement google api code - throw UnimplementedError( - 'This feature is not supported by Google Classroom. Please contact the developer.'); - } - - @override - Future> getAssignmentGrades(int assignmentId) async { - print('Getting assignment grades...'); - print('Assignment ID: $assignmentId'); - // TODO: implement google api code - throw UnimplementedError( - 'This feature is not supported by Google Classroom. Please contact the developer.'); - } - - @override - Future setRubricGrades( - int assignmentId, int userId, String jsonGrades) async { - print('Setting rubric grades...'); - print('Assignment ID: $assignmentId'); - print('User ID: $userId'); - // TODO: implement google api code - throw UnimplementedError( - 'This feature is not supported by Google Classroom. Please contact the developer.'); - } - - @override - Future> getRubricGrades(int assignmentId, int userid) async { - print('Getting rubric grades...'); - print('Assignment ID: $assignmentId'); - print('User ID: $userid'); - // TODO: implement google api code - throw UnimplementedError( - 'This feature is not supported by Google Classroom. Please contact the developer.'); - } - - @override - Grade? findGradeForUser(List grades, int userId) { - print('Finding grade for user...'); - print('User ID: $userId'); - - // TODO: implement google api code - throw UnimplementedError( - 'This feature is not supported by Google Classroom. Please contact the developer.'); - } - - // **************************************************************************************** - // Rubric retrieval - // **************************************************************************************** - - @override - Future getRubric(String assignmentid) async { - // TODO: implement google api code - print('Getting rubric...'); - print('Assignment ID: $assignmentid'); - throw UnimplementedError( - 'This feature is not supported by Google Classroom. Please contact the developer.'); - } - // **************************************************************************************** - // Quiz creation and assignment with Answer Key - //Short answer question 10 Points - //Multiple choice question 10 Points - //True/False question 5 Points - - // **************************************************************************************** - - Future createAndAssignQuizFromXml( - String courseId, - String quizName, - String quizDescription, - String quizAsXml, - String dueDate, - ) async { - try { - if (quizAsXml.isEmpty) { - print('Error: quizAsXml is empty.'); - return false; - } - - final document = xml.XmlDocument.parse(quizAsXml); - final questions = document.findAllElements('question').toList(); - String? teacherFolderId; - - for (Course course in courses!) { - if (course.id == int.parse(courseId)) { - teacherFolderId = course.teacherFolderId; - } - } - - Map? formResponse = - await _classroomApi.createForm(teacherFolderId, quizName); - if (formResponse == null) { - print('Error: Failed to create Google Form.'); - return false; - } - - final String formId = formResponse['formId']; - final String responderUri = formResponse['responderUri']; - - List> requests = []; - requests.add({ - 'updateSettings': { - 'settings': { - 'emailCollectionType': 'DO_NOT_COLLECT', - 'quizSettings': {'isQuiz': true} - }, - 'updateMask': 'email_collection_type,quiz_settings', - } - }); - - // Parse and add questions with answer keys and points - for (var questionElement in questions) { - String questionType = questionElement.getAttribute('type') ?? 'unknown'; - String questionText = questionElement - .getElement('questiontext') - ?.getElement('text') - ?.text ?? - ''; - - if (questionType == 'category') { - print( - 'Warning: Unsupported question type: $questionType. Skipping question.'); - continue; - } - switch (questionType) { - case 'multichoice': - int points = 10; // Set multichoice to 10 points - List options = []; - List correctAnswerIndices = []; - var answerElements = - questionElement.findAllElements('answer').toList(); - for (int i = 0; i < answerElements.length; i++) { - var answerElement = answerElements[i]; - String optionText = answerElement.getElement('text')?.text ?? ''; - options.add(optionText); - if (answerElement.getAttribute('fraction') == '100') { - correctAnswerIndices.add(i); - } - } - requests.add(_createMultipleChoiceQuestionRequest( - questionText, options, correctAnswerIndices, points)); - break; - case 'truefalse': - int points = 5; // Set truefalse to 5 points - String correctAnswer = questionElement - .findAllElements('answer') - .firstWhere((e) => e.getAttribute('fraction') == '100') - .getElement('text') - ?.text ?? - 'True'; - requests.add(_createTrueFalseQuestionRequest( - questionText, correctAnswer, points)); - break; - case 'shortanswer': - int points = 10; // Set shortanswer to 10 point - String correctAnswer = questionElement - .findAllElements('answer') - .first - .getElement('text') - ?.text ?? - ''; - requests.add(_createShortAnswerQuestionRequest( - questionText, correctAnswer, points)); - break; - default: - print('Warning: Unsupported question type: $questionType'); - } - } - - Map? batchResponse = - await _classroomApi.batchUpdateForm(formId, requests); - if (batchResponse == null) { - print('Error: Failed to update Google Form.'); - return false; - } - - String? assignmentId = await _classroomApi.createAssignment( - courseId, - quizName, - quizDescription, - responderUri, - dueDate, - ); - - if (assignmentId == null) { - print('Error: Failed to create Classroom assignment.'); - return false; - } - - print( - 'Quiz created and assigned successfully! Assignment ID: $assignmentId'); - return true; - } catch (e) { - print('Error during quiz creation and assignment: $e'); - return false; - } - } - - Map _createMultipleChoiceQuestionRequest(String questionText, - List options, List correctAnswerIndices, int points) { - List> choices = []; - for (String option in options) { - choices.add({'value': option}); - } - - return { - 'createItem': { - 'item': { - 'title': questionText, - 'questionItem': { - 'question': { - 'required': true, - 'choiceQuestion': { - 'type': 'RADIO', - 'options': choices, - }, - 'grading': { - 'pointValue': points, - 'correctAnswers': { - 'answers': correctAnswerIndices - .map((index) => {'value': options[index]}) - .toList(), - }, - }, - }, - }, - }, - 'location': {'index': 0}, - }, - }; - } - - Map _createTrueFalseQuestionRequest( - String questionText, String correctAnswer, int points) { - List options = ["True", "False"]; - return { - 'createItem': { - 'item': { - 'title': questionText, - 'questionItem': { - 'question': { - 'required': true, - 'choiceQuestion': { - 'type': 'RADIO', - 'options': [ - {'value': 'True'}, - {'value': 'False'}, - ], - }, - 'grading': { - 'pointValue': points, - 'correctAnswers': { - 'answers': [ - {'value': correctAnswer}, - ], - }, - }, - }, - }, - }, - 'location': {'index': 0}, - }, - }; - } - - Map _createShortAnswerQuestionRequest( - String questionText, String correctAnswer, int points) { - return { - 'createItem': { - 'item': { - 'title': questionText, - 'questionItem': { - 'question': { - 'required': true, - 'textQuestion': {}, - 'grading': { - 'pointValue': points, - 'correctAnswers': { - 'answers': [ - {'value': correctAnswer}, - ], - }, - }, - }, - }, - }, - 'location': {'index': 0}, - }, - }; - } - - @override - Future> getQuizGradesForParticipants( - String courseId, int quizId) { - // TODO: implement getQuizGradesForParticipants - throw UnimplementedError(); - } -} diff --git a/team_a/teamA/lib/Api/lms/lms_interface.dart b/team_a/teamA/lib/Api/lms/lms_interface.dart deleted file mode 100644 index 28a932c6..00000000 --- a/team_a/teamA/lib/Api/lms/lms_interface.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:learninglens_app/beans/course.dart'; -import 'package:learninglens_app/beans/quiz.dart'; -import 'package:learninglens_app/beans/quiz_type.dart'; -import 'package:learninglens_app/beans/assignment.dart'; -import 'package:learninglens_app/beans/participant.dart'; -import 'package:learninglens_app/beans/submission_status.dart'; -import 'package:learninglens_app/beans/grade.dart'; -import 'package:learninglens_app/beans/submission.dart'; -import 'package:learninglens_app/beans/submission_with_grade.dart'; -import 'package:learninglens_app/beans/moodle_rubric.dart'; - -// Singleton interface class for API access. -abstract class LmsInterface { - late String serverUrl; - - // User info - late String apiURL; - String? userName; - String? firstName; - String? lastName; - String? siteName; - String? fullName; - String? profileImage; - List? courses; - - // Authentication/Login methods - Future login(String username, String password, String baseURL); - bool isLoggedIn(); - Future isUserTeacher(List moodleCourses); - void logout(); - void resetLMSUserInfo(); - - // Course methods - Future> getCourses(); - Future> getUserCourses(); - Future> getCourseParticipants(String courseId); - - // Quiz methods - Future importQuiz(String courseid, String quizXml); - Future> getQuizzes(int? courseID, {int? topicId}); - Future createQuiz(String courseid, String quizname, String quizintro, - String sectionid, String timeopen, String timeclose); - Future addRandomQuestions( - String categoryid, String quizid, String numquestions); - Future importQuizQuestions(String courseid, String quizXml); - Future> getQuestionsFromQuiz(int quizId); - - // Assignment methods - Future> getEssays(int? courseID, {int? topicId}); - Future?> createAssignment(String courseid, String sectionid, String assignmentName, - String startdate, String enddate, String rubricJson, String description); - Future getContextId(int assignmentId, String courseId); - - // Submission and grading methods - Future> getAssignmentSubmissions(int assignmentId); - Future> getSubmissionsWithGrades(int assignmentId); - Future getSubmissionStatus(int assignmentId, int userId); - Future> getAssignmentGrades(int assignmentId); - Future setRubricGrades(int assignmentId, int userId, String jsonGrades); - Future> getRubricGrades(int assignmentId, int userid); - Grade? findGradeForUser(List grades, int userId); - - // Rubric methods - Future getRubric(String assignmentid); - Future> getQuizGradesForParticipants(String courseId, int quizId); -} diff --git a/team_a/teamA/lib/Api/lms/moodle/classroom_service.dart b/team_a/teamA/lib/Api/lms/moodle/classroom_service.dart deleted file mode 100644 index b9f78bb0..00000000 --- a/team_a/teamA/lib/Api/lms/moodle/classroom_service.dart +++ /dev/null @@ -1 +0,0 @@ -// TODO Implement this library. \ No newline at end of file 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 deleted file mode 100644 index 2f04ecbb..00000000 --- a/team_a/teamA/lib/Api/lms/moodle/moodle_lms_service.dart +++ /dev/null @@ -1,1298 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:learninglens_app/Api/lms/lms_interface.dart'; -import 'package:learninglens_app/beans/course.dart'; -import 'package:learninglens_app/beans/lesson_plan.dart'; -import 'package:learninglens_app/beans/question_stat_type.dart'; -import 'package:learninglens_app/beans/quiz.dart'; -import 'package:learninglens_app/beans/override.dart'; -import 'package:learninglens_app/beans/assignment.dart'; -import 'package:learninglens_app/beans/participant.dart'; -import 'package:learninglens_app/beans/quiz_override'; -import 'package:learninglens_app/beans/quiz_type.dart'; -import 'package:learninglens_app/beans/submission_status.dart'; -import 'package:learninglens_app/beans/grade.dart'; -import 'package:learninglens_app/beans/submission.dart'; -import 'package:learninglens_app/beans/submission_with_grade.dart'; -import 'package:learninglens_app/beans/moodle_rubric.dart'; -import 'package:learninglens_app/services/api_service.dart'; - -/// A Singleton class for Moodle API access implementing [LmsInterface]. -class MoodleLmsService implements LmsInterface { - // **************************************************************************************** - // Static / Singleton internals - // **************************************************************************************** - - static final MoodleLmsService _instance = MoodleLmsService._internal(); - - /// The singleton accessor. - factory MoodleLmsService() => _instance; - - /// Private named constructor. - MoodleLmsService._internal(); - - // **************************************************************************************** - // Fields implementing ApiSingletonInterface - // **************************************************************************************** - - @override - String serverUrl = '/webservice/rest/server.php'; // The Moodle REST endpoint - - // The user token is kept private (not in the interface). - String? _userToken; - - @override - String apiURL = - ''; // Base URL for your Moodle instance, e.g. "https://yourmoodle.com" - @override - String? userName; - @override - String? firstName; - @override - String? lastName; - @override - String? siteName; - @override - String? fullName; - @override - String? profileImage; - @override - List? courses; - - List? overrides; - - // **************************************************************************************** - // Auth / Login - // **************************************************************************************** - - @override - Future login(String username, String password, String baseURL) async { - print('Logging in to Moodle...'); - - // 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')); - - if (response.statusCode != 200) { - throw HttpException(response.body); - } - - final data = jsonDecode(response.body) as Map; - if (data.containsKey('error')) { - throw HttpException(data['error']); - } - - // Store token locally - _userToken = data['token']; - apiURL = baseURL; - - // 2) Retrieve user info - final userinforesponse = - await ApiService().httpPost(Uri.parse(apiURL + serverUrl), body: { - 'wstoken': _userToken, - 'wsfunction': 'core_webservice_get_site_info', - 'moodlewsrestformat': 'json', - }); - - if (userinforesponse.statusCode != 200) { - throw HttpException(userinforesponse.body); - } - - // 3) Parse user info - final userData = jsonDecode(userinforesponse.body) as Map; - userName = userData['username']; - firstName = userData['firstname']; - lastName = userData['lastname']; - siteName = userData['sitename']; - fullName = userData['fullname']; - profileImage = userData['userpictureurl']; - - // 4) Optionally load user courses right away - courses = await getUserCourses(); - overrides = await getAssignmentOverrides(); - } - - @override - bool isLoggedIn() { - return _userToken != null; - } - - @override - Future isUserTeacher(List moodleCourses) async { - // Check each course to see if the user has a teacher role - for (var course in moodleCourses) { - final rolesResponse = - await ApiService().httpPost(Uri.parse(apiURL + serverUrl), body: { - 'wstoken': _userToken, - 'wsfunction': 'core_enrol_get_enrolled_users', - 'courseid': course.id.toString(), - 'moodlewsrestformat': 'json', - }); - - if (rolesResponse.statusCode != 200) { - throw Exception('Failed to load roles for course ${course.id}'); - } - - // If the user has roleid == 3 or 4, they are teacher-like roles - final users = jsonDecode(rolesResponse.body) as List; - for (var user in users) { - if (user['username'].toString() == userName) { - for (var role in user['roles']) { - if (role['roleid'] == 3 || role['roleid'] == 4) { - return true; - } - } - } - } - } - return false; - } - - @override - void logout() { - print('Logging out of Moodle...'); - resetLMSUserInfo(); - } - - @override - void resetLMSUserInfo() { - // Clear all user-related fields - _userToken = null; - apiURL = ''; - userName = null; - firstName = null; - lastName = null; - siteName = null; - fullName = null; - profileImage = null; - courses = []; - } - - // **************************************************************************************** - // Course-related methods - // **************************************************************************************** - - @override - Future> getCourses() async { - // Returns all courses on the Moodle site (requires admin or special permissions) - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final response = - await ApiService().httpPost(Uri.parse(apiURL + serverUrl), body: { - 'wstoken': _userToken, - 'wsfunction': 'core_course_get_courses', - 'moodlewsrestformat': 'json', - }); - - if (response.statusCode != 200) { - throw HttpException(response.body); - } - - final listData = jsonDecode(response.body) as List; - return listData.map((i) => Course.empty().fromMoodleJson(i)).toList(); - } - - @override - Future> getUserCourses() async { - // Returns courses the user is enrolled in - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final response = - await ApiService().httpPost(Uri.parse(apiURL + serverUrl), body: { - 'wstoken': _userToken, - 'wsfunction': - 'core_course_get_enrolled_courses_by_timeline_classification', - 'classification': 'inprogress', - 'moodlewsrestformat': 'json', - }); - - if (response.statusCode != 200) { - throw HttpException(response.body); - } - - final decodedJson = jsonDecode(response.body); - List userCourses; - - // The response can be either a List or a Map with a 'courses' key - if (decodedJson is List) { - userCourses = - decodedJson.map((i) => Course.empty().fromMoodleJson(i)).toList(); - } else if (decodedJson is Map) { - final courseList = decodedJson['courses'] as List; - userCourses = - courseList.map((i) => Course.empty().fromMoodleJson(i)).toList(); - } else { - throw StateError('Unexpected response format from Moodle'); - } - - // Optionally fetch quizzes/essays for each course - for (Course c in userCourses) { - c.quizzes = await getQuizzes(c.id); - c.essays = await getEssays(c.id); - } - return userCourses; - } - - Future> getAssignmentOverrides() async { - // Returns courses the user is enrolled in - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final response = - await ApiService().httpPost(Uri.parse(apiURL + serverUrl), body: { - 'wstoken': _userToken, - 'wsfunction': 'local_learninglens_get_all_overrides', - 'moodlewsrestformat': 'json', - }); - - if (response.statusCode != 200) { - throw HttpException(response.body); - } - - final decodedJson = jsonDecode(response.body); - List assignmentOverrides; - - if (decodedJson is List) { - assignmentOverrides = - decodedJson.map((i) => Override.empty().fromMoodleJson(i)).toList(); - } else if (decodedJson is Map) { - final courseList = decodedJson['courses'] as List; - assignmentOverrides = - courseList.map((i) => Override.empty().fromMoodleJson(i)).toList(); - } else { - throw StateError('Unexpected response format from Moodle'); - } - - return assignmentOverrides; - } - - @override - Future> getCourseParticipants(String courseId) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final response = - await ApiService().httpPost(Uri.parse(apiURL + serverUrl), body: { - 'wstoken': _userToken, - 'wsfunction': 'core_enrol_get_enrolled_users', - 'courseid': courseId, - 'moodlewsrestformat': 'json', - }); - - if (response.statusCode != 200) { - throw HttpException(response.body); - } - - final decodedJson = jsonDecode(response.body); - List participants = []; - if (decodedJson is List) { - for (var participant in decodedJson) { - if (isStudent(participant)) { - participants.add(Participant.empty().fromMoodleJson(participant)); - } - } - return participants; - // return decodedJson - // .map((i) => Participant.empty().fromMoodleJson(i)) - // .toList(); - } else { - throw StateError('Unexpected response format (expected a List)'); - } - } - - bool isStudent(Map json) { - // Moodle student role id equals 5 - bool isStudent = false; - for (Map role in json['roles']) { - if (role['roleid'] == 5) { - isStudent = true; - } - } - return isStudent; - } - - // **************************************************************************************** - // Quiz methods - // **************************************************************************************** - - @override - Future importQuiz(String courseid, String quizXml) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken, - 'wsfunction': 'local_quizgen_import_questions', - 'moodlewsrestformat': 'json', - 'courseid': courseid, - 'questionxml': quizXml, - }, - ); - - if (response.statusCode != 200) { - throw HttpException(response.body); - } - // Check if Moodle returned an 'error' inside the JSON string - if (response.body.contains('error')) { - throw HttpException(response.body); - } - } - - @override - Future> getQuizzes(int? courseID, {int? topicId}) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken, - 'wsfunction': 'mod_quiz_get_quizzes_by_courses', - 'moodlewsrestformat': 'json', - }, - ); - - if (response.statusCode != 200) { - throw HttpException(response.body); - } - - final quizzesMap = jsonDecode(response.body) as Map; - final decodedJson = quizzesMap['quizzes'] as List?; - - if (decodedJson == null) { - return []; - } - - List quizList = []; - for (var item in decodedJson) { - // If courseID is null, return all quizzes; otherwise filter by course - if (courseID == null || item['course'] == courseID) { - quizList.add(Quiz( - name: item['name'], - coursedId: item['course'], - description: item['intro'], - id: item['id'], - timeOpen: item['timeopen'] = - DateTime.fromMillisecondsSinceEpoch(item['timeopen'] * 1000), - timeClose: item['timeclose'] = - DateTime.fromMillisecondsSinceEpoch(item['timeclose'] * 1000))); - } - } - return quizList; - } - - @override - Future createQuiz(String courseid, String quizname, String quizintro, - String sectionid, String timeopen, String timeclose) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - try { - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken, - 'wsfunction': 'local_learninglens_create_quiz', - 'moodlewsrestformat': 'json', - 'courseid': courseid, - 'name': quizname, - 'intro': quizintro, - 'sectionid': sectionid, - 'timeopen': timeopen, - 'timeclose': timeclose, - }, - ); - - if (response.statusCode != 200) { - print('Failed to create quiz. Status code: ${response.statusCode}.'); - return null; - } - - final responseData = jsonDecode(response.body) as Map; - print('Create Quiz Response: $responseData'); - - return responseData['quizid']; - } catch (e) { - print('Error creating quiz: $e'); - return null; - } - } - - @override - Future addRandomQuestions( - String categoryid, String quizid, String numquestions) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken, - 'wsfunction': 'local_learninglens_add_type_randoms_to_quiz', - 'moodlewsrestformat': 'json', - 'categoryid': categoryid, - 'quizid': quizid, - 'numquestions': numquestions, - }, - ); - - if (response.statusCode != 200) { - return 'Request failed with status: ${response.statusCode}.'; - } - - // The response may be a simple boolean string or a JSON object - try { - if (response.body == 'true' || response.body == 'false') { - final boolString = (response.body == 'true').toString(); - print('Boolean Response: $boolString'); - return boolString; - } else { - final responseData = jsonDecode(response.body) as Map; - print('Response: $responseData'); - return responseData['status']; - } - } catch (e) { - print('Error parsing response in addRandomQuestions: $e'); - return e.toString(); - } - } - - @override - Future importQuizQuestions(String courseid, String quizXml) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - try { - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken, - 'wsfunction': 'local_learninglens_import_questions', - 'moodlewsrestformat': 'json', - 'courseid': courseid, - 'questionxml': quizXml, - }, - ); - - if (response.statusCode != 200) { - print('Request failed with status: ${response.statusCode}.'); - return null; - } - - // Some Moodle plugins return extra text before JSON; parse from first '{' - final int indexOfBrace = response.body.indexOf('{'); - if (indexOfBrace == -1) { - print('No JSON object found in response for importQuizQuestions.'); - return null; - } - final jsonPart = response.body.substring(indexOfBrace); - final responseData = jsonDecode(jsonPart) as Map; - print('Import Questions Response: $responseData'); - - return responseData['categoryid'] as int?; - } catch (e) { - print('Error importing quiz questions: $e'); - return null; - } - } - - // **************************************************************************************** - // Assignment methods - // **************************************************************************************** - - @override - Future> getEssays(int? courseID, {int? topicId}) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken, - 'wsfunction': 'mod_assign_get_assignments', - 'moodlewsrestformat': 'json', - }, - ); - - if (response.statusCode != 200) { - throw HttpException(response.body); - } - - final mapJson = jsonDecode(response.body) as Map; - final coursesList = mapJson['courses'] as List?; - - if (coursesList == null) { - return []; - } - - final results = []; - for (var cItem in coursesList) { - // If courseID is null, get for all courses; if not, filter - if (courseID == null || cItem['id'] == courseID) { - final assignmentList = cItem['assignments'] as List; - for (var a in assignmentList) { - results.add(Assignment.empty().fromMoodleJson(a)); - } - } - } - return results; - } - - @override - Future?> createAssignment( - String courseid, - String sectionid, - String assignmentName, - String startdate, - String enddate, - String rubricJson, - String description, - ) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - try { - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken, - 'wsfunction': 'local_learninglens_create_assignment', - 'moodlewsrestformat': 'json', - 'courseid': courseid, - 'sectionid': sectionid, - 'assignmentName': assignmentName, - 'startdate': startdate, - 'enddate': enddate, - 'rubricJson': rubricJson, - 'description': description, - }, - ); - - if (response.statusCode != 200) { - print('Request failed with status: ${response.statusCode}.'); - return null; - } - - final responseData = jsonDecode(response.body) as Map; - print('Create Assignment Response: $responseData'); - - return responseData; - } catch (e) { - print('Error occurred while creating assignment: $e'); - return null; - } - } - - @override - Future getContextId(int assignmentId, String courseId) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - try { - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken, - 'wsfunction': 'core_course_get_contents', - 'moodlewsrestformat': 'json', - 'courseid': courseId, - }, - ); - - if (response.statusCode != 200) { - print( - 'Failed to fetch course contents. Status code: ${response.statusCode}'); - return null; - } - - final data = jsonDecode(response.body) as List; - for (var section in data) { - final modules = section['modules'] as List; - for (var module in modules) { - // Look for an assignment module with matching instance - if (module['instance'] == assignmentId && - module['modname'] == 'assign') { - final contextId = module['contextid']; - print('Context ID for assignment $assignmentId is $contextId'); - return contextId as int?; - } - } - } - return null; - } catch (e, st) { - print('Error fetching context ID: $e'); - print('StackTrace: $st'); - return null; - } - } - - // **************************************************************************************** - // Submissions and grading - // **************************************************************************************** - - @override - Future> getAssignmentSubmissions(int assignmentId) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - try { - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken, - 'wsfunction': 'mod_assign_get_submissions', - 'moodlewsrestformat': 'json', - 'assignmentids[0]': assignmentId.toString(), - 'status': 'submitted', - }, - ); - - if (response.statusCode != 200) { - print( - 'Failed to load submissions. Status code: ${response.statusCode}'); - return []; - } - - final data = jsonDecode(response.body) as Map; - if (data.containsKey('exception')) { - throw Exception('Moodle API Error: ${data['message']}'); - } - - // Debugging - print('Full Response Data (Submissions): ${jsonEncode(data)}'); - - final assignmentsArr = data['assignments'] as List? ?? []; - final submissionsData = >[]; - - // Extract each "submission" plus the assignmentId - for (var assign in assignmentsArr) { - final subs = assign['submissions'] as List? ?? []; - for (var sub in subs) { - submissionsData.add({ - 'assignmentid': assign['assignmentid'], - 'submission': sub, - }); - } - } - - if (submissionsData.isEmpty) { - print('No submissions found for assignment $assignmentId'); - return []; - } - - return submissionsData - .map((jsonMap) => Submission.empty().fromMoodleJson(jsonMap)) - .toList(); - } catch (e, st) { - print('Error fetching submissions: $e'); - print('StackTrace: $st'); - return []; - } - } - - @override - Future> getSubmissionsWithGrades( - int assignmentId) async { - // Combine submissions with their associated grade - final submissions = await getAssignmentSubmissions(assignmentId); - final grades = await getAssignmentGrades(assignmentId); - - final results = []; - for (final submission in submissions) { - final match = findGradeForUser(grades, submission.userid); - results.add(SubmissionWithGrade(submission: submission, grade: match)); - } - return results; - } - - @override - Future getSubmissionStatus( - int assignmentId, int userId) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - try { - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken, - 'wsfunction': 'mod_assign_get_submission_status', - 'moodlewsrestformat': 'json', - 'assignid': assignmentId.toString(), - 'userid': userId.toString(), - }, - ); - - if (response.statusCode != 200) { - print( - 'Failed to load submission status. Status code: ${response.statusCode}'); - return null; - } - - final data = jsonDecode(response.body) as Map; - if (data.containsKey('exception')) { - throw Exception('Moodle API Error: ${data['message']}'); - } - - return SubmissionStatus.empty().fromMoodleJson(data); - } catch (e, st) { - print('Error fetching submission status: $e'); - print('StackTrace: $st'); - return null; - } - } - - @override - Future> getAssignmentGrades(int assignmentId) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - try { - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken, - 'wsfunction': 'mod_assign_get_grades', - 'moodlewsrestformat': 'json', - 'assignmentids[0]': assignmentId.toString(), - }, - ); - - if (response.statusCode != 200) { - print( - 'Failed to load assignment grades. Status code: ${response.statusCode}'); - return []; - } - - final data = jsonDecode(response.body) as Map; - if (data.containsKey('exception')) { - throw Exception('Moodle API Error: ${data['message']}'); - } - - print('Grades Response Data: ${jsonEncode(data)}'); - - final grades = []; - final assignmentsList = data['assignments'] as List? ?? []; - for (var assignment in assignmentsList) { - final gList = assignment['grades'] as List? ?? []; - for (var g in gList) { - grades.add(Grade.empty().fromMoodleJson(g)); - } - } - return grades; - } catch (e, st) { - print('Error fetching assignment grades: $e'); - print('StackTrace: $st'); - return []; - } - } - - @override - Future setRubricGrades( - int assignmentId, int userId, String jsonGrades) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - try { - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken, - 'wsfunction': 'local_learninglens_write_rubric_grades', - 'moodlewsrestformat': 'json', - 'assignmentid': assignmentId.toString(), - 'userid': userId.toString(), - 'rubricgrades': jsonGrades, - }, - ); - - if (response.statusCode != 200) { - print( - 'Failed to set rubric grades. Status code: ${response.statusCode}'); - return false; - } - return true; - } catch (e, st) { - print('Error setting rubric grades: $e'); - print('StackTrace: $st'); - return false; - } - } - - @override - Future> getRubricGrades(int assignmentId, int userid) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - try { - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken, - 'wsfunction': 'local_learninglens_get_rubric_grades', - 'moodlewsrestformat': 'json', - 'assignmentid': assignmentId.toString(), - 'userid': userid.toString(), - }, - ); - - if (response.statusCode != 200) { - print('Failed to load rubric grades. Status: ${response.statusCode}'); - return []; - } - - final data = jsonDecode(response.body) as List; - if (data.isEmpty || data.first is! Map) { - return []; - } - print('Rubric Grades Response: ${jsonEncode(data)}'); - return data; - } catch (e, st) { - print('Error fetching rubric grades: $e'); - print('StackTrace: $st'); - return []; - } - } - - @override - Grade? findGradeForUser(List grades, int userId) { - for (final grade in grades) { - if (grade.userid == userId) { - return grade; - } - } - return null; - } - - // **************************************************************************************** - // Rubric retrieval - // **************************************************************************************** - - @override - Future getRubric(String assignmentid) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken, - 'wsfunction': 'local_learninglens_get_rubric', - 'moodlewsrestformat': 'json', - 'assignmentid': assignmentid, - }, - ); - - if (response.statusCode != 200) { - print('Request failed with status: ${response.statusCode}.'); - return null; - } - - final List responseData = jsonDecode(response.body); - if (responseData.isEmpty || responseData.first is! Map) { - return null; - } - - print('Rubric JSON Response: $responseData'); - return MoodleRubric.empty().fromMoodleJson(responseData.first); - } - - // **************************************************************************************** - // TODO: add the method below to the lms_interface. - // **************************************************************************************** - /** - * Fetches all the questions from a quiz. - */ - Future> getQuestionsFromQuiz(int quizId) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final url = Uri.parse('$apiURL$serverUrl'); - - final response = await ApiService().httpPost( - url, - body: { - 'wstoken': _userToken!, - 'wsfunction': 'local_learninglens_get_questions_from_quiz', - 'moodlewsrestformat': 'json', - 'quizid': quizId.toString(), - }, - ); - - if (response.statusCode == 200) { - final List jsonList = json.decode(response.body); - - return jsonList - .map((json) => QuestionType.empty().fromMoodleJson(json)) - .toList(); - } else { - throw Exception("Failed to fetch questions: ${response.body}"); - } - } - - Future addQuizOverride({ - required int quizId, - int? userId, - int? groupId, - int? timeOpen, - int? timeClose, - int? timeLimit, - int? attempts, - String? password, - }) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final url = Uri.parse('$apiURL$serverUrl'); - - // Dynamically build request body, removing null values - final Map body = { - 'wstoken': _userToken!, - 'wsfunction': 'local_learninglens_add_quiz_override', - 'moodlewsrestformat': 'json', - 'quizid': quizId.toString(), - }; - - // Add only non-null fields - if (userId != null) body['userid'] = userId.toString(); - if (groupId != null) body['groupid'] = groupId.toString(); - if (timeOpen != null) body['timeopen'] = timeOpen.toString(); - if (timeClose != null) body['timeclose'] = timeClose.toString(); - if (timeLimit != null) body['timelimit'] = timeLimit.toString(); - if (attempts != null) body['attempts'] = attempts.toString(); - if (password != null && password.isNotEmpty) body['password'] = password; - - final response = await ApiService().httpPost(url, body: body); - - final responseData = json.decode(response.body); - - if (response.statusCode == 200 && responseData is Map) { - return QuizOverride.empty().fromMoodleJson(responseData); - } else { - throw Exception("Failed to create quiz override: ${response.body}"); - } - } - - Future addEssayOverride({ - required int assignid, - int? userId, - int? groupId, - int? allowsubmissionsfromdate, - int? dueDate, - int? cutoffDate, - int? timelimit, - int? sortorder, - }) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final url = Uri.parse('$apiURL$serverUrl'); - - // Dynamically build request body, removing null values - final Map body = { - 'wstoken': _userToken!, - 'wsfunction': 'local_learninglens_add_essay_override', - 'moodlewsrestformat': 'json', - 'assignid': assignid.toString(), - }; - - // Add only non-null fields - if (userId != null) body['userid'] = userId.toString(); - if (groupId != null) body['groupid'] = groupId.toString(); - if (allowsubmissionsfromdate != null) - body['allowsubmissionsfromdate'] = allowsubmissionsfromdate.toString(); - if (dueDate != null) body['duedate'] = dueDate.toString(); - if (cutoffDate != null) body['cutoffdate'] = cutoffDate.toString(); - if (timelimit != null) body['timelimit'] = timelimit.toString(); - if (sortorder != null) body['sortorder'] = sortorder.toString(); - - final response = await ApiService().httpPost(url, body: body); - - final responseData = json.decode(response.body); - - if (response.statusCode == 200 && responseData is Map) { - return 'Override: ${responseData['override_id']}'; - } else { - throw Exception("Failed to create quiz override: ${response.body}"); - } - } - -/** - * Fetches all lesson plans associated with a given course ID. - */ - Future> getLessonPlans(int? courseId) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final url = Uri.parse('$apiURL$serverUrl'); - - final response = await ApiService().httpPost( - url, - body: { - 'wstoken': _userToken!, - 'wsfunction': 'local_learninglens_get_lesson_plans_by_course', - 'moodlewsrestformat': 'json', - 'courseid': courseId.toString(), - }, - ); - - // print("Lesson Plans Response: ${response.body}"); - - if (response.statusCode == 200) { - final List jsonList = json.decode(response.body); - return jsonList - .map((json) => LessonPlan.empty().fromMoodleJson(json)) - .toList(); - } else { - throw Exception("Failed to fetch lesson plans: ${response.body}"); - } - } - -// Method to create the lesson in Moodle - Future createLesson({ - required int courseId, - required String lessonPlanName, - required String content, - int introformat = 1, - int showdescription = 1, - int available = 0, - int deadline = 0, - int timelimit = 0, - int retake = 1, - int maxAttempts = 3, - int usepassword = 0, - String password = '', - int completion = 1, - }) async { - try { - print("Creating lesson with courseId: $courseId, name: $lessonPlanName"); - - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - "wstoken": _userToken, - "wsfunction": "local_learninglens_create_lesson", - "moodlewsrestformat": "json", - // REQUIRED fields - "courseid": courseId.toString(), - "name": lessonPlanName, - "intro": content, - // OPTIONAL fields - "introformat": introformat.toString(), - "showdescription": showdescription.toString(), - "available": available.toString(), - "deadline": deadline.toString(), - "timelimit": timelimit.toString(), - "retake": retake.toString(), - "maxattempts": maxAttempts.toString(), - "usepassword": usepassword.toString(), - "password": password, - "completion": completion.toString(), - }, - ); - - final jsonResponse = json.decode(response.body) as Map; - print("createLesson Response: $jsonResponse"); - - if (jsonResponse.containsKey("exception")) { - print("Error creating lesson: ${jsonResponse['message']}"); - return false; - } - - print( - "Lesson created successfully! Lesson ID: ${jsonResponse['lessonId']}"); - print("Linked to Course Module ID: ${jsonResponse['courseModuleId']}"); - - return true; - } catch (e) { - print("Error occurred while creating lesson: $e"); - return false; - } - } - - /** - * Deletes a lesson plan from Moodle by its lesson ID. - */ - Future deleteLessonPlan(int lessonId) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final url = Uri.parse('$apiURL$serverUrl'); - - final response = await ApiService().httpPost( - url, - body: { - 'wstoken': _userToken!, - 'wsfunction': 'local_learninglens_delete_lesson_plan', - 'moodlewsrestformat': 'json', - 'lessonid': lessonId.toString(), - }, - ); - - print("Delete Lesson Plan Response: ${response.body}"); - - if (response.statusCode == 200) { - final Map jsonResponse = json.decode(response.body); - - if (jsonResponse.containsKey("status") && - jsonResponse["status"] == "success") { - print("Lesson plan deleted successfully."); - return true; - } else { - print("Error deleting lesson plan: ${jsonResponse['message']}"); - return false; - } - } else { - throw Exception("Failed to delete lesson plan: ${response.body}"); - } - } - - /** - * Updates a lesson plan in Moodle by its lesson ID. - */ - Future updateLessonPlan({ - required int lessonId, - String? name, - String? intro, - int? available, - int? deadline, - }) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - final url = Uri.parse('$apiURL$serverUrl'); - - // Construct request body dynamically - final Map body = { - 'wstoken': _userToken!, - 'wsfunction': 'local_learninglens_update_lesson_plan', - 'moodlewsrestformat': 'json', - 'lessonid': lessonId.toString(), - }; - - if (name != null) body['name'] = name; - if (intro != null) body['intro'] = intro; - if (available != null) body['available'] = available.toString(); - if (deadline != null) body['deadline'] = deadline.toString(); - - final response = await ApiService().httpPost(url, body: body); - - print("Update Lesson Plan Response: ${response.body}"); - - if (response.statusCode == 200) { - final Map jsonResponse = json.decode(response.body); - - if (jsonResponse.containsKey("status") && - jsonResponse["status"] == "success") { - print("Lesson plan updated successfully."); - return true; - } else { - print("Error updating lesson plan: ${jsonResponse['message']}"); - return false; - } - } else { - throw Exception("Failed to update lesson plan: ${response.body}"); - } - } - - Future> getQuizGradesForParticipants( - String courseId, int quizId) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - // Get all course participants (filtered to only students) - List students = (await getCourseParticipants(courseId)) - .where((participant) => participant.roles.contains('student')) - .toList(); - - for (var student in students) { - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken!, - 'wsfunction': 'mod_quiz_get_user_best_grade', - 'moodlewsrestformat': 'json', - 'quizid': quizId.toString(), - 'userid': student.id.toString(), - }, - ); - - if (response.statusCode != 200) { - print( - 'Failed to get grade for user ${student.id}. Status: ${response.statusCode}'); - continue; - } - - final jsonResponse = jsonDecode(response.body); - if (jsonResponse is Map && - jsonResponse.containsKey('grade')) { - student.avgGrade = double.tryParse(jsonResponse['grade'].toString()); - } else { - student.avgGrade = null; // No grade found - } - } - return students; - } - - Future> getEssayGradesForParticipants( - String courseId, int assignmentId) async { - if (_userToken == null) throw StateError('User not logged in to Moodle'); - - // Get all course participants (filtered to only students) - List students = (await getCourseParticipants(courseId)) - .where((participant) => participant.roles.contains('student')) - .toList(); - - for (var student in students) { - final response = await ApiService().httpPost( - Uri.parse(apiURL + serverUrl), - body: { - 'wstoken': _userToken!, - 'wsfunction': 'mod_assign_get_grades', - 'moodlewsrestformat': 'json', - 'assignmentids[0]': assignmentId.toString(), - }, - ); - - if (response.statusCode != 200) { - print( - 'Failed to get grade for user ${student.id}. Status: ${response.statusCode}'); - continue; - } - - final jsonResponse = jsonDecode(response.body); - if (jsonResponse is Map && - jsonResponse.containsKey('assignments')) { - final assignments = jsonResponse['assignments'] as List; - for (var assignment in assignments) { - final grades = assignment['grades'] as List; - for (var grade in grades) { - if (grade['userid'] == student.id) { - student.avgGrade = double.tryParse(grade['grade'].toString()); - break; - } - } - } - } else { - student.avgGrade = null; // No grade found - } - } - return students; - } - - - /// Fetches extended question stats (correct/incorrect/partial attempts) - /// from the local_learninglens_get_question_stats_from_quiz service. - Future> getQuestionStatsFromQuiz(int quizId) async { - if (_userToken == null) { - throw StateError('User not logged in to Moodle'); - } - - final url = Uri.parse('$apiURL$serverUrl'); - - final response = await ApiService().httpPost( - url, - body: { - 'wstoken': _userToken!, - 'wsfunction': 'local_learninglens_get_question_stats_from_quiz', - 'moodlewsrestformat': 'json', - 'quizid': quizId.toString(), - }, - ); - - if (response.statusCode != 200) { - throw Exception('Failed to fetch question stats: ${response.body}'); - } - print(quizId); - print(response.body); - final decoded = json.decode(response.body); - if (decoded is! List) { - throw Exception( - 'Expected a list of question stats but got: ${response.body}', - ); - } - - // Convert each item into a QuestionStatsType instance - return decoded.map((item) { - return QuestionStatsType.fromJson(item); - }).toList(); - } -} diff --git a/team_a/teamA/lib/Api/lms/template/api_singleton.dart b/team_a/teamA/lib/Api/lms/template/api_singleton.dart deleted file mode 100644 index 5c60ebd9..00000000 --- a/team_a/teamA/lib/Api/lms/template/api_singleton.dart +++ /dev/null @@ -1,236 +0,0 @@ -import 'package:learninglens_app/beans/course.dart'; -import 'package:learninglens_app/beans/lesson_plan.dart'; -import 'package:learninglens_app/beans/quiz.dart'; -import 'package:learninglens_app/beans/quiz_type.dart'; -import 'package:learninglens_app/beans/assignment.dart'; -import 'package:learninglens_app/beans/participant.dart'; -import 'package:learninglens_app/beans/submission_status.dart'; -import 'package:learninglens_app/beans/grade.dart'; -import 'package:learninglens_app/beans/submission.dart'; -import 'package:learninglens_app/beans/submission_with_grade.dart'; -import 'package:learninglens_app/beans/moodle_rubric.dart'; -import 'package:learninglens_app/Api/lms/lms_interface.dart'; - - -class ApiSingleton implements LmsInterface { - static final ApiSingleton _instance = ApiSingleton._internal(); - - ApiSingleton._internal(); - - factory ApiSingleton() { - return _instance; - } - - String? _userToken = ''; - - @override - String serverUrl = ''; - - @override - String apiURL = ''; - - @override - String? userName; - - @override - String? firstName; - - @override - String? lastName; - - @override - String? siteName; - - @override - String? fullName; - - @override - String? profileImage; - - @override - List? courses; - - - // Authentication/Login methods - @override - Future login(String username, String password, String baseURL) { - // TODO: implement login - throw UnimplementedError(); - } - - @override - bool isLoggedIn() { - return _userToken != null; - } - - @override - Future isUserTeacher(List moodleCourses) { - // TODO: implement isUserTeacher - throw UnimplementedError(); - } - - @override - void logout() { - print('Logging out of LMS...'); - resetLMSUserInfo(); - } - - @override - void resetLMSUserInfo() { - _userToken = null; - apiURL = ''; - userName = null; - firstName = null; - lastName = null; - siteName = null; - fullName = null; - profileImage = null; - courses = []; - } - - // Course methods - @override - Future> getCourses() { - // TODO: implement getCourses - throw UnimplementedError(); - } - - @override - Future> getUserCourses() { - // TODO: implement getUserCourses - throw UnimplementedError(); - } - - @override - Future> getCourseParticipants(String courseId) { - // TODO: implement getCourseParticipants - throw UnimplementedError(); - } - - - // Quiz methods - @override - Future importQuiz(String courseid, String quizXml) { - // TODO: implement importQuiz - throw UnimplementedError(); - } - - @override - Future> getQuizzes(int? courseID, {int? topicId}) { - // TODO: implement getQuizzes - throw UnimplementedError(); - } - - @override - Future createQuiz(String courseid, String quizname, String quizintro, String sectionid, String timeopen, String timeclose) { - // TODO: implement createQuiz - throw UnimplementedError(); - } - - @override - Future importQuizQuestions(String courseid, String quizXml) { - // TODO: implement importQuizQuestions - throw UnimplementedError(); - } - - @override - Future addRandomQuestions(String categoryid, String quizid, String numquestions) { - // TODO: implement addRandomQuestions - throw UnimplementedError(); - } - - @override - Future> getQuestionsFromQuiz(int quizId) async { - // TODO: implement google api code - throw UnimplementedError(); - } - - - // Assignment methods - @override - Future> getEssays(int? courseID, {int? topicId}) { - // TODO: implement getEssays - throw UnimplementedError(); - } - - @override - Future?> createAssignment(String courseid, String sectionid, String assignmentName, String startdate, String enddate, String rubricJson, String description) { - // TODO: implement createAssignment - throw UnimplementedError(); - } - - @override - Future getContextId(int assignmentId, String courseId) { - // TODO: implement getContextId - throw UnimplementedError(); - } - - // Submission and grading methods - @override - Future> getAssignmentSubmissions(int assignmentId) { - // TODO: implement getAssignmentSubmissions - throw UnimplementedError(); - } - - @override - Future> getSubmissionsWithGrades(int assignmentId) { - // TODO: implement getSubmissionsWithGrades - throw UnimplementedError(); - } - - @override - Future getSubmissionStatus(int assignmentId, int userId) { - // TODO: implement getSubmissionStatus - throw UnimplementedError(); - } - - @override - Future> getAssignmentGrades(int assignmentId) { - // TODO: implement getAssignmentGrades - throw UnimplementedError(); - } - - @override - Future setRubricGrades(int assignmentId, int userId, String jsonGrades) { - // TODO: implement setRubricGrades - throw UnimplementedError(); - } - - @override - Future getRubricGrades(int assignmentId, int userid) { - // TODO: implement getRubricGrades - throw UnimplementedError(); - } - - @override - Grade? findGradeForUser(List grades, int userId) { - // TODO: implement findGradeForUser - throw UnimplementedError(); - } - - // Rubric methods - @override - Future getRubric(String assignmentid) { - // TODO: implement getRubric - throw UnimplementedError(); - } - - //Get Lesson Plan - FuturegetLessonPlan(String courseId){ - //\TODO: implement getLessonPlan - throw UnimplementedError(); - } - - @override - Future> getQuestionsFromQuizGoogle(String courseId, String courseWorkId) async { - // TODO: implement google api code - throw UnimplementedError(); - } - - @override - Future> getQuizGradesForParticipants(String courseId, int quizId) { - // TODO: implement getQuizGradesForParticipants - throw UnimplementedError(); - } - -} \ No newline at end of file diff --git a/team_a/teamA/lib/Controller/assessment_generator.dart b/team_a/teamA/lib/Controller/assessment_generator.dart deleted file mode 100644 index a8a97c46..00000000 --- a/team_a/teamA/lib/Controller/assessment_generator.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'dart:convert'; -import 'package:learninglens_app/beans/quiz.dart'; -import 'package:learninglens_app/services/api_service.dart'; - -class AssessmentGenerator -{ - final String serverUrl; - - AssessmentGenerator({required this.serverUrl}); - - Future generateAssessment(String queryPrompt) async { - final response = await ApiService().httpPost( - Uri.parse('$serverUrl/generateAssessment'), - headers: {'Content-Type': 'application/x-www-form-urlencoded'}, - body: {'QueryPrompt': queryPrompt}, - ); - - if (response.statusCode == 200) { - final responseBody = json.decode(response.body); - final aiResponse = responseBody['assessment']; - return Quiz.fromXmlString(aiResponse); - } else { - throw Exception('Failed to generate assessment'); - } - } -} diff --git a/team_a/teamA/lib/Controller/custom_appbar.dart b/team_a/teamA/lib/Controller/custom_appbar.dart deleted file mode 100644 index b212a208..00000000 --- a/team_a/teamA/lib/Controller/custom_appbar.dart +++ /dev/null @@ -1,195 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:learninglens_app/Api/experimental/assistant/textbased_function_caller_view.dart'; -import 'package:learninglens_app/Api/lms/enum/lms_enum.dart'; -import 'package:learninglens_app/Views/dashboard.dart'; -import 'package:learninglens_app/Views/user_settings.dart'; -import 'package:learninglens_app/Views/chat_screen.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; - -class ClassroomSelection { - static LmsType selectedClassroom = LocalStorageService.getSelectedClassroom() == LmsType.MOODLE - ? LmsType.MOODLE - : LmsType.GOOGLE; -} - -class CustomAppBar extends StatefulWidget implements PreferredSizeWidget { - final String title; - final String userprofileurl; - final VoidCallback? onRefresh; // Optional refresh callback - - CustomAppBar({required this.title, required this.userprofileurl, this.onRefresh}); - - @override - _CustomAppBarState createState() => _CustomAppBarState(); - - @override - Size get preferredSize => Size.fromHeight(kToolbarHeight); -} - -class _CustomAppBarState extends State { - @override - Widget build(BuildContext context) { - return AppBar( - backgroundColor: Theme.of(context).colorScheme.primaryContainer, - title: Text( - widget.title, - textAlign: TextAlign.center, - ), - centerTitle: true, - leadingWidth: 120, - leading: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Flexible( - child: IconButton( - icon: Icon(Icons.arrow_back), - onPressed: isDashboard(context) - ? null - : () { - Navigator.pop(context); - }, - ), - ), - Flexible( - child: IconButton( - icon: Icon(Icons.home), - onPressed: isDashboard(context) - ? null - : () { - navigateToSelectedDashboard(context); - }, - ), - ), - Flexible( - child: IconButton( - icon: Icon(Icons.chat_rounded), - onPressed: () { - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => ChatScreen() - ) - ); - }, - ), - ), - ], - ), - actions: [ - - Flexible( - child: IconButton( - icon: Icon(Icons.science), // Science Icon - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => TextBasedFunctionCallerView()), - ); - }, - ), - ), - // Refresh button: Instead of relying on an external callback, - // try to obtain the current route's name and then replace the route, - // effectively refreshing the current view. - IconButton( - icon: Icon(Icons.refresh), - onPressed: () { - final currentRouteName = ModalRoute.of(context)?.settings.name; - if (currentRouteName != null) { - Navigator.pushReplacementNamed(context, currentRouteName); - } else { - // Fallback: if no route name is available, call the provided onRefresh callback if any. - widget.onRefresh?.call(); - } - }, - ), - DropdownButtonHideUnderline( - child: DropdownButton( - value: ClassroomSelection.selectedClassroom, - onChanged: isDashboard(context) - ? (LmsType? newValue) { - if (newValue != null) { - setState(() { - ClassroomSelection.selectedClassroom = newValue; - }); - navigateToSelectedDashboard(context); - } - } - : null, - items: LmsType.values.map>((LmsType value) { - return DropdownMenuItem( - value: value, - child: Text( - value == LmsType.GOOGLE ? 'Google Classroom' : 'Moodle Classroom', - ), - ); - }).toList(), - ), - ), - IconButton( - icon: Icon(Icons.settings), - onPressed: !isDashboard(context) - ? null - : () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => UserSettings()), - ); - }, - ), - Padding( - padding: EdgeInsets.only(right: 10.0), - child: InkWell( - child: Material( - color: Colors.transparent, - child: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - shape: BoxShape.circle, - ), - child: ClipOval( - child: Image.network( - widget.userprofileurl, - height: 50, - width: 50, - fit: BoxFit.cover, - errorBuilder: (BuildContext context, Object error, StackTrace? stackTrace) { - // print('Image load error: $error'); - // Just display the account_circle icon be default - return Icon(Icons.account_circle, size: 50); - }, - ), - ), - ), - ), - ), - ), - ], - ); - } - - bool isDashboard(BuildContext context) { - return widget.title == 'Learning Lens'; - } - - void navigateToSelectedDashboard(BuildContext context) { - if (ClassroomSelection.selectedClassroom == LmsType.GOOGLE) { - LocalStorageService.saveSelectedClassroom(LmsType.GOOGLE); - } else { - LocalStorageService.saveSelectedClassroom(LmsType.MOODLE); - } - - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => TeacherDashboard(), - // builder: (context) => ClassroomSelection.selectedClassroom == LmsType.GOOGLE - // // ? GoogleTeacherDashboard() - // ? TeacherDashboard() - // : TeacherDashboard(), - ), - ); - } -} diff --git a/team_a/teamA/lib/Controller/essay_generator.dart b/team_a/teamA/lib/Controller/essay_generator.dart deleted file mode 100644 index 18b5370b..00000000 --- a/team_a/teamA/lib/Controller/essay_generator.dart +++ /dev/null @@ -1,223 +0,0 @@ -import 'dart:convert'; -import 'package:flutter/services.dart'; -import 'package:flutter/material.dart'; -import 'package:learninglens_app/beans/criterion.dart'; - -class Essay -{ - //Holds the assignment prompt - String? prompt; - //Holds the course level information - String? courseLevel; - //Holds the list of crtiera for a given assignment - List criteria; - //Constructor for a standard essay - Essay(this.prompt, this.courseLevel, this.criteria); - //Generates the table data for the rubric based on the criteria for the essay object. Does not create header row. - List> genTableData() - { - List critList = criteria; - List> tableData = []; - for (Criterion c in critList) - { - int iterator = 1; - Map tempMap = {}; - String currName = c.name; - num currWeight = c.points; - Map desc = c.descriptions; - for (MapEntry d in desc.entries) { - if (iterator == 1) { - tempMap[''] = '$currName\n\nPoints: $currWeight'; - iterator++; - } - tempMap[d.key] = d.value; - iterator++; - } - tableData.add(tempMap); - } - return tableData; - } - //Creates the header row for the table data - List genHeaderData() - { - List headerData = criteria[0].descriptions.keys.toList(); - headerData.insert(0, ''); - return headerData; - } -} - - -// Fetch Criteria data from JSON file and add it to the program -Future> fetchJsonCriteria() async -{ - final String jsonString = await rootBundle.loadString('assets/Criteria.json'); - final Map jsonData = json.decode(jsonString); - List critList = jsonData['Criteria']; // Access the 'users' key - return critList.map((critJson) => Criterion.fromJson(critJson)).toList(); -} - -// Take the default descriptions from the JSON and build full list of critieria based on scale -Future> generateCriteria( - int scale, List selectedCriteria) async { - List allCriteria = await fetchJsonCriteria() as List; - List myCriteria = []; - int iterator = 0; - for (Criterion c in allCriteria) { - List newValues = []; - if (selectedCriteria.contains(c.name)) { - myCriteria.add(c); - switch (scale) { - case 3: - for (int i = 0; i <= 2; i++) { - String currScale = c.scale3[i]; - String currDesc = c.defaultDesc; - newValues.add("Student displays $currScale $currDesc"); - } - myCriteria[iterator].addDescriptions(c.scale3, newValues); - myCriteria[iterator].setWeight(3); - iterator++; - case 4: - for (int i = 0; i <= 3; i++) { - String currScale = c.scale4[i]; - String currDesc = c.defaultDesc; - newValues.add("Student displays $currScale $currDesc"); - } - myCriteria[iterator].addDescriptions(c.scale4, newValues); - myCriteria[iterator].setWeight(4); - iterator++; - case 5: - for (int i = 0; i <= 4; i++) { - String currScale = c.scale5[i]; - String currDesc = c.defaultDesc; - newValues.add("Student displays $currScale $currDesc"); - } - myCriteria[iterator].addDescriptions(c.scale5, newValues); - myCriteria[iterator].setWeight(5); - iterator++; - } - } - } - return myCriteria; -} - -//App that constructs the table widget -class TableApp extends StatelessWidget -{ - const TableApp(this.tableData, this.headerData); - final List> tableData; - final List headerData; - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Editable Table', - home: Scaffold( - appBar: AppBar( - title: Text('Rubric'), - ), - body: Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: EditableTable(tableData: tableData, headerData: headerData), - ), - ), - ), - ); - } -} - -//Object that creates editable table rows -class EditableTable extends StatefulWidget -{ - final List> tableData; - final List headerData; - EditableTable({required this.tableData, required this.headerData}); - @override - _EditableTableState createState() => _EditableTableState(); -} - -class _EditableTableState extends State -{ - late List> controllers; - @override - void initState() { - super.initState(); - // Create controllers for each cell in the table except the first column - controllers = widget.tableData.map((row) { - return row.map((key, value) { - return MapEntry(key, TextEditingController(text: value)); - }); - }).toList(); - } - @override - Widget build(BuildContext context) { - return Table( - border: TableBorder.all(), - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: _buildTableRows(), - ); - } - // Function to build the table rows dynamically - List _buildTableRows() { - List rows = []; - // Add table header row dynamically based on column headers - rows.add( - TableRow( - children: widget.headerData.map((header) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Text(header, - textAlign: TextAlign.center, - style: TextStyle(fontWeight: FontWeight.bold)), - ); - }).toList(), - ), - ); - // Add rows dynamically based on data - for (var i = 0; i < widget.tableData.length; i++) { - rows.add( - TableRow( - children: widget.headerData.map((header) { - // If it is the first column, display it as static text - if (header == widget.headerData.first) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Text(widget.tableData[i][header] ?? '', - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.bold, - )), - ); - } - // For other columns, make them editable - return Padding( - padding: const EdgeInsets.all(8.0), - child: TextFormField( - controller: controllers[i][header], - onChanged: (value) { - widget.tableData[i][header] = value; // Update the data - }, - decoration: InputDecoration( - border: OutlineInputBorder(), - ), - keyboardType: TextInputType.multiline, - maxLines: null, // This allows the text field to wrap and expand - minLines: 1, // Minimum number of lines the field should show - ), - ); - }).toList(), - ), - ); - } - return rows; - } - @override - void dispose() { - // Dispose of all controllers when the widget is disposed - for (var row in controllers) { - for (var controller in row.values) { - controller.dispose(); - } - } - super.dispose(); - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/Controller/g_bean.dart b/team_a/teamA/lib/Controller/g_bean.dart deleted file mode 100644 index a83045a3..00000000 --- a/team_a/teamA/lib/Controller/g_bean.dart +++ /dev/null @@ -1,787 +0,0 @@ -class GCourse { - String id; - String fullName; - - GCourse({required this.id, required this.fullName}); - - factory GCourse.fromJson(Map json) { - return GCourse( - id: json['id'] as String, - fullName: json['name'] - as String, // Use 'name' to match the Google Classroom API - ); - } -} - -// beans.dart - -class Course { - String id; - String name; - String? section; - String? descriptionHeading; - String? description; - String? room; - String? ownerId; - String? creationTime; - String? updateTime; - String? enrollmentCode; - String? courseState; // Consider using an enum for CourseState - String? alternateLink; - String? teacherGroupEmail; - String? courseGroupEmail; - DriveFolder? teacherFolder; - List? courseMaterialSets; - bool? guardiansEnabled; - String? calendarId; - GradebookSettings? gradebookSettings; - - Course({ - required this.id, - required this.name, - this.section, - this.descriptionHeading, - this.description, - this.room, - this.ownerId, - this.creationTime, - this.updateTime, - this.enrollmentCode, - this.courseState, - this.alternateLink, - this.teacherGroupEmail, - this.courseGroupEmail, - this.teacherFolder, - this.courseMaterialSets, - this.guardiansEnabled, - this.calendarId, - this.gradebookSettings, - }); - - factory Course.fromJson(Map json) { - return Course( - id: json['id'] as String, - name: json['name'] as String, - section: json['section'] as String?, - descriptionHeading: json['descriptionHeading'] as String?, - description: json['description'] as String?, - room: json['room'] as String?, - ownerId: json['ownerId'] as String?, - creationTime: json['creationTime'] as String?, - updateTime: json['updateTime'] as String?, - enrollmentCode: json['enrollmentCode'] as String?, - courseState: json['courseState'] as String?, // Consider using an enum - alternateLink: json['alternateLink'] as String?, - teacherGroupEmail: json['teacherGroupEmail'] as String?, - courseGroupEmail: json['courseGroupEmail'] as String?, - teacherFolder: json['teacherFolder'] == null - ? null - : DriveFolder.fromJson(json['teacherFolder'] as Map), - courseMaterialSets: (json['courseMaterialSets'] as List?) - ?.map((e) => CourseMaterialSet.fromJson(e as Map)) - .toList(), - guardiansEnabled: json['guardiansEnabled'] as bool?, - calendarId: json['calendarId'] as String?, - gradebookSettings: json['gradebookSettings'] == null - ? null - : GradebookSettings.fromJson( - json['gradebookSettings'] as Map), - ); - } - - Map toJson() { - return { - 'id': id, - 'name': name, - 'section': section, - 'descriptionHeading': descriptionHeading, - 'description': description, - 'room': room, - 'ownerId': ownerId, - 'creationTime': creationTime, - 'updateTime': updateTime, - 'enrollmentCode': enrollmentCode, - 'courseState': courseState, - 'alternateLink': alternateLink, - 'teacherGroupEmail': teacherGroupEmail, - 'courseGroupEmail': courseGroupEmail, - 'teacherFolder': teacherFolder?.toJson(), - 'courseMaterialSets': courseMaterialSets?.map((e) => e.toJson()).toList(), - 'guardiansEnabled': guardiansEnabled, - 'calendarId': calendarId, - 'gradebookSettings': gradebookSettings?.toJson(), - }; - } - - @override - String toString() { - return 'Course{id: $id, name: $name, section: $section}'; - } -} - -class DriveFolder { - String? id; - String? title; - String? alternateLink; - - DriveFolder({this.id, this.title, this.alternateLink}); - - factory DriveFolder.fromJson(Map json) { - return DriveFolder( - id: json['id'] as String?, - title: json['title'] as String?, - alternateLink: json['alternateLink'] as String?, - ); - } - - Map toJson() { - return { - 'id': id, - 'title': title, - 'alternateLink': alternateLink, - }; - } -} - -class CourseMaterialSet { - String? title; - - CourseMaterialSet({this.title}); - - factory CourseMaterialSet.fromJson(Map json) { - return CourseMaterialSet( - title: json['title'] as String?, - ); - } - - Map toJson() { - return { - 'title': title, - }; - } -} - -class GradebookSettings { - // Define properties based on the GradebookSettings object - // Example: - String? categoryId; - - GradebookSettings({this.categoryId}); - - factory GradebookSettings.fromJson(Map json) { - return GradebookSettings( - categoryId: json['categoryId'] as String?, - ); - } - - Map toJson() { - return { - 'categoryId': categoryId, - }; - } -} - -// CourseWork Bean -class CourseWork { - String? courseId; - String? id; - String? title; - String? description; - List? materials; - String? state; // Consider enum for CourseWorkState - String? alternateLink; - String? creationTime; - String? updateTime; - Date? dueDate; - TimeOfDay? dueTime; - String? scheduledTime; - double? maxPoints; - String? workType; // Consider enum for CourseWorkType - bool? associatedWithDeveloper; - String? assigneeMode; // Consider enum for AssigneeMode - IndividualStudentsOptions? individualStudentsOptions; - String? submissionModificationMode; // Consider enum - String? creatorUserId; - String? topicId; - GradeCategory? gradeCategory; - String? gradingPeriodId; - Assignment? assignment; - MultipleChoiceQuestion? multipleChoiceQuestion; - String? previewVersion; - - CourseWork( - {this.courseId, - this.id, - this.title, - this.description, - this.materials, - this.state, - this.alternateLink, - this.creationTime, - this.updateTime, - this.dueDate, - this.dueTime, - this.scheduledTime, - this.maxPoints, - this.workType, - this.associatedWithDeveloper, - this.assigneeMode, - this.individualStudentsOptions, - this.submissionModificationMode, - this.creatorUserId, - this.topicId, - this.gradeCategory, - this.gradingPeriodId, - this.assignment, - this.multipleChoiceQuestion, - this.previewVersion}); - - factory CourseWork.fromJson(Map json) { - return CourseWork( - courseId: json['courseId'] as String?, - id: json['id'] as String?, - title: json['title'] as String?, - description: json['description'] as String?, - materials: (json['materials'] as List?) - ?.map((e) => Material.fromJson(e as Map)) - .toList(), - state: json['state'] as String?, - alternateLink: json['alternateLink'] as String?, - creationTime: json['creationTime'] as String?, - updateTime: json['updateTime'] as String?, - dueDate: json['dueDate'] == null - ? null - : Date.fromJson(json['dueDate'] as Map), - dueTime: json['dueTime'] == null - ? null - : TimeOfDay.fromJson(json['dueTime'] as Map), - scheduledTime: json['scheduledTime'] as String?, - maxPoints: (json['maxPoints'] as num?)?.toDouble(), - workType: json['workType'] as String?, - associatedWithDeveloper: json['associatedWithDeveloper'] as bool?, - assigneeMode: json['assigneeMode'] as String?, - individualStudentsOptions: json['individualStudentsOptions'] == null - ? null - : IndividualStudentsOptions.fromJson( - json['individualStudentsOptions'] as Map), - submissionModificationMode: json['submissionModificationMode'] as String?, - creatorUserId: json['creatorUserId'] as String?, - topicId: json['topicId'] as String?, - gradeCategory: json['gradeCategory'] == null - ? null - : GradeCategory.fromJson( - json['gradeCategory'] as Map), - gradingPeriodId: json['gradingPeriodId'] as String?, - assignment: json['assignment'] == null - ? null - : Assignment.fromJson(json['assignment'] as Map), - multipleChoiceQuestion: json['multipleChoiceQuestion'] == null - ? null - : MultipleChoiceQuestion.fromJson( - json['multipleChoiceQuestion'] as Map), - previewVersion: json['previewVersion'] as String?, - ); - } - Map toJson() { - return { - 'courseId': courseId, - 'id': id, - 'title': title, - 'description': description, - 'materials': materials?.map((e) => e.toJson()).toList(), - 'state': state, - 'alternateLink': alternateLink, - 'creationTime': creationTime, - 'updateTime': updateTime, - 'dueDate': dueDate?.toJson(), - 'dueTime': dueTime?.toJson(), - 'scheduledTime': scheduledTime, - 'maxPoints': maxPoints, - 'workType': workType, - 'associatedWithDeveloper': associatedWithDeveloper, - 'assigneeMode': assigneeMode, - 'individualStudentsOptions': individualStudentsOptions?.toJson(), - 'submissionModificationMode': submissionModificationMode, - 'creatorUserId': creatorUserId, - 'topicId': topicId, - 'gradeCategory': gradeCategory?.toJson(), - 'gradingPeriodId': gradingPeriodId, - 'assignment': assignment?.toJson(), - 'multipleChoiceQuestion': multipleChoiceQuestion?.toJson(), - 'previewVersion': previewVersion, - }; - } - - @override - String toString() { - return 'CourseWork{id: $id, title: $title, description: $description}'; - } -} - -// Material Bean -class Material { - String? title; - String? alternateLink; - - Material({this.title, this.alternateLink}); - - factory Material.fromJson(Map json) { - return Material( - title: json['title'] as String?, - alternateLink: json['alternateLink'] as String?, - ); - } - - Map toJson() { - return { - 'title': title, - 'alternateLink': alternateLink, - }; - } - - @override - String toString() { - return 'Material{title: $title, alternateLink: $alternateLink}'; - } -} - -// Date Bean -class Date { - int? year; - int? month; - int? day; - - Date({this.year, this.month, this.day}); - - factory Date.fromJson(Map json) { - return Date( - year: json['year'] as int?, - month: json['month'] as int?, - day: json['day'] as int?, - ); - } - - Map toJson() { - return { - 'year': year, - 'month': month, - 'day': day, - }; - } - - @override - String toString() { - return 'Date{year: $year, month: $month, day: $day}'; - } -} - -// TimeOfDay Bean -class TimeOfDay { - int? hours; - int? minutes; - int? seconds; - int? nanos; - - TimeOfDay({this.hours, this.minutes, this.seconds, this.nanos}); - - factory TimeOfDay.fromJson(Map json) { - return TimeOfDay( - hours: json['hours'] as int?, - minutes: json['minutes'] as int?, - seconds: json['seconds'] as int?, - nanos: json['nanos'] as int?, - ); - } - - Map toJson() { - return { - 'hours': hours, - 'minutes': minutes, - 'seconds': seconds, - 'nanos': nanos, - }; - } - - @override - String toString() { - return 'TimeOfDay{hours: $hours, minutes: $minutes}'; - } -} - -// IndividualStudentsOptions Bean -class IndividualStudentsOptions { - // Add properties as needed based on the API definition - - IndividualStudentsOptions(); - - factory IndividualStudentsOptions.fromJson(Map json) { - return IndividualStudentsOptions(); - } - - Map toJson() { - return {}; - } -} - -// GradeCategory Bean -class GradeCategory { - String? name; - double? defaultPoints; - - GradeCategory({this.name, this.defaultPoints}); - - factory GradeCategory.fromJson(Map json) { - return GradeCategory( - name: json['name'] as String?, - defaultPoints: (json['defaultPoints'] as num?)?.toDouble(), - ); - } - - Map toJson() { - return { - 'name': name, - 'defaultPoints': defaultPoints, - }; - } - - @override - String toString() { - return 'GradeCategory{name: $name, defaultPoints: $defaultPoints}'; - } -} - -// Assignment Bean -class Assignment { - // Add properties as needed based on the API definition - // Example: - String? instructions; - - Assignment({this.instructions}); - - factory Assignment.fromJson(Map json) { - return Assignment( - instructions: json['instructions'] as String?, - ); - } - - Map toJson() { - return { - 'instructions': instructions, - }; - } -} - -// MultipleChoiceQuestion Bean -class MultipleChoiceQuestion { - // Add properties as needed based on the API definition - List? choices; - - MultipleChoiceQuestion({this.choices}); - - factory MultipleChoiceQuestion.fromJson(Map json) { - return MultipleChoiceQuestion( - choices: - (json['choices'] as List?)?.map((e) => e as String).toList(), - ); - } - - Map toJson() { - return { - 'choices': choices, - }; - } -} - -// UserProfile Bean (Used in Student and Teacher) -class UserProfile { - String? id; - Name? name; - String? emailAddress; - String? photoUrl; - - UserProfile({this.id, this.name, this.emailAddress, this.photoUrl}); - - factory UserProfile.fromJson(Map json) { - return UserProfile( - id: json['id'] as String?, - name: json['name'] == null - ? null - : Name.fromJson(json['name'] as Map), - emailAddress: json['emailAddress'] as String?, - photoUrl: json['photoUrl'] as String?, - ); - } - - Map toJson() { - return { - 'id': id, - 'name': name?.toJson(), - 'emailAddress': emailAddress, - 'photoUrl': photoUrl, - }; - } -} - -// Name Bean (Used within UserProfile) -class Name { - String? familyName; - String? givenName; - String? fullName; - - Name({this.familyName, this.givenName, this.fullName}); - - factory Name.fromJson(Map json) { - return Name( - familyName: json['familyName'] as String?, - givenName: json['givenName'] as String?, - fullName: json['fullName'] as String?, - ); - } - - Map toJson() { - return { - 'familyName': familyName, - 'givenName': givenName, - 'fullName': fullName, - }; - } -} - -// Student Bean -class Student { - String courseId; - String userId; - UserProfile? profile; - DriveFolder? studentWorkFolder; - - Student( - {required this.courseId, - required this.userId, - this.profile, - this.studentWorkFolder}); - - factory Student.fromJson(Map json) { - return Student( - courseId: json['courseId'] as String, - userId: json['userId'] as String, - profile: json['profile'] == null - ? null - : UserProfile.fromJson(json['profile'] as Map), - studentWorkFolder: json['studentWorkFolder'] == null - ? null - : DriveFolder.fromJson( - json['studentWorkFolder'] as Map), - ); - } - - Map toJson() { - return { - 'courseId': courseId, - 'userId': userId, - 'profile': profile?.toJson(), - 'studentWorkFolder': studentWorkFolder?.toJson(), - }; - } -} - -// Teacher Bean -class Teacher { - String courseId; - String userId; - UserProfile? profile; - - Teacher({required this.courseId, required this.userId, this.profile}); - - factory Teacher.fromJson(Map json) { - return Teacher( - courseId: json['courseId'] as String, - userId: json['userId'] as String, - profile: json['profile'] == null - ? null - : UserProfile.fromJson(json['profile'] as Map), - ); - } - - Map toJson() { - return { - 'courseId': courseId, - 'userId': userId, - 'profile': profile?.toJson(), - }; - } -} - -//courses.topics Bean -class Topic { - String courseId; - String topicId; - String name; - String updateTime; - - Topic( - {required this.courseId, - required this.topicId, - required this.name, - required this.updateTime}); - - factory Topic.fromJson(Map json) { - return Topic( - courseId: json['courseId'] as String, - topicId: json['topicId'] as String, - name: json['name'] as String, - updateTime: json['updateTime'] as String, - ); - } - - Map toJson() { - return { - 'courseId': courseId, - 'topicId': topicId, - 'name': name, - 'updateTime': updateTime, - }; - } -} - -//courses.courseWork.rubrics Bean -class Rubric { - String courseId; - String courseWorkId; - String id; - String creationTime; - String updateTime; - List? criteria; - String? sourceSpreadsheetId; //Union field - - Rubric( - {required this.courseId, - required this.courseWorkId, - required this.id, - required this.creationTime, - required this.updateTime, - this.criteria, - this.sourceSpreadsheetId}); - - factory Rubric.fromJson(Map json) { - return Rubric( - courseId: json['courseId'] as String, - courseWorkId: json['courseWorkId'] as String, - id: json['id'] as String, - creationTime: json['creationTime'] as String, - updateTime: json['updateTime'] as String, - criteria: (json['criteria'] as List?) - ?.map((e) => Criterion.fromJson(e as Map)) - .toList(), - sourceSpreadsheetId: json['sourceSpreadsheetId'] as String?, - ); - } - - Map toJson() { - return { - 'courseId': courseId, - 'courseWorkId': courseWorkId, - 'id': id, - 'creationTime': creationTime, - 'updateTime': updateTime, - 'criteria': criteria?.map((e) => e.toJson()).toList(), - 'sourceSpreadsheetId': sourceSpreadsheetId, - }; - } -} - -// Criterion Bean (Used in Rubric) -class Criterion { - String? criterionId; - String? description; - List? levels; - - Criterion({this.criterionId, this.description, this.levels}); - - factory Criterion.fromJson(Map json) { - return Criterion( - criterionId: json['criterionId'] as String?, - description: json['description'] as String?, - levels: (json['levels'] as List?) - ?.map((e) => Level.fromJson(e as Map)) - .toList(), - ); - } - - Map toJson() { - return { - 'criterionId': criterionId, - 'description': description, - 'levels': levels?.map((e) => e.toJson()).toList(), - }; - } -} - -// Level Bean (Used in Criterion) -class Level { - String? levelId; - String? description; - String? title; - double? points; - - Level({this.levelId, this.description, this.title, this.points}); - - factory Level.fromJson(Map json) { - return Level( - levelId: json['levelId'] as String?, - description: json['description'] as String?, - title: json['title'] as String?, - points: (json['points'] as num?)?.toDouble(), - ); - } - - Map toJson() { - return { - 'levelId': levelId, - 'description': description, - 'title': title, - 'points': points, - }; - } -} - -//Enum for CourseState -enum CourseState { - COURSE_STATE_UNSPECIFIED, - ACTIVE, - ARCHIVED, - PROVISIONED, - DECLINED -} - -//Enum for CourseWorkState -enum CourseWorkState { - COURSE_WORK_STATE_UNSPECIFIED, - PUBLISHED, - DRAFT, - DELETED -} - -//Enum for CourseWorkType -enum CourseWorkType { - COURSE_WORK_TYPE_UNSPECIFIED, - ASSIGNMENT, - SHORT_ANSWER_QUESTION, - MULTIPLE_CHOICE_QUESTION -} - -//Enum for AssigneeMode -enum AssigneeMode { - ASSIGNEE_MODE_UNSPECIFIED, - ALL_STUDENTS, - INDIVIDUAL_STUDENTS -} - -//Enum for SubmissionModificationMode -enum SubmissionModificationMode { - SUBMISSION_MODIFICATION_MODE_UNSPECIFIED, - MODIFIABLE_UNTIL_TURNED_IN, - MODIFIABLE_AFTER_TURNED_IN -} - -//Enum for PreviewVersion -enum PreviewVersion { PREVIEW_VERSION_UNSPECIFIED, PUBLISHED_VERSION } diff --git a/team_a/teamA/lib/Controller/html_converter.dart b/team_a/teamA/lib/Controller/html_converter.dart deleted file mode 100644 index 403ee8c2..00000000 --- a/team_a/teamA/lib/Controller/html_converter.dart +++ /dev/null @@ -1,17 +0,0 @@ -class HtmlConverter -{ - - // Convert string by replacing common HTML tags. - static String convert(String htmlString) { - return htmlString.replaceAll('

', '\n') - .replaceAll('

', '\n') - .replaceAll('
  • ', '\n') - .replaceAll('
  • ', '\n') - .replaceAll('
      ', '\n') - .replaceAll('
    ', '\n') - .replaceAll('', '') - .replaceAll('', '') - .replaceAll('', '') - .replaceAll('', ''); - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/Controller/main_controller.dart b/team_a/teamA/lib/Controller/main_controller.dart deleted file mode 100644 index e53faad7..00000000 --- a/team_a/teamA/lib/Controller/main_controller.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:learninglens_app/beans/course.dart'; - -class MainController { - // Singleton instance - static final MainController _instance = MainController._internal(); - // Singleton accessor - factory MainController() { - return _instance; - } - // Internal constructor - MainController._internal(); - static bool isLoggedIn = false; - // final llm = LlmApi(dotenv.env['PERPLEXITY_API_KEY']!); - final ValueNotifier isUserLoggedInNotifier = ValueNotifier(false); - Course? selectedCourse; - String? username; - - -} diff --git a/team_a/teamA/lib/Controller/xml_converter.dart b/team_a/teamA/lib/Controller/xml_converter.dart deleted file mode 100644 index c2559ff1..00000000 --- a/team_a/teamA/lib/Controller/xml_converter.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:collection/collection.dart'; -import 'html_converter.dart'; -import 'package:xml/xml.dart'; -import 'package:learninglens_app/beans/quiz.dart'; -import 'package:learninglens_app/beans/xml_consts.dart'; -import 'package:learninglens_app/beans/question.dart'; -import 'package:learninglens_app/beans/answer.dart'; - -// Static utility class for Moodle XML related functions. -class XmlConverter { - - static const cdata1 = ''; - - // Convert a populated Quiz object into an XML Document. - // - // To get the XML String, use XmlDocument.toXmlString() - static XmlDocument convertQuizToXml(Quiz quiz, [bool? toMoodle]) { - bool convertForMoodle = toMoodle ?? false; - final builder = XmlBuilder(); - builder.processing('xml', 'version="1.0" encoding="UTF-8"'); - builder.element(XmlConsts.quiz, nest: () { - // build category dummy node - builder.element(XmlConsts.name, nest: quiz.name ?? 'Unnamed Quiz'); - builder.element(XmlConsts.description, nest: quiz.description ?? 'No description'); - builder.element(XmlConsts.question, nest: () { - builder.attribute(XmlConsts.type, 'category'); - builder.element('category', nest: () { - builder.element(XmlConsts.text, nest: '\$course\$/top/LLM-Generated'); - }); - }); - for (Question question in quiz.questionList) { - builder.element(XmlConsts.question, nest: () { - // question type - builder.attribute(XmlConsts.type, question.type); - // question name - builder.element(XmlConsts.name, nest: () { - if (convertForMoodle) { - builder.element(XmlConsts.text, nest: getShortenedDesc(question.questionText)); - } else { - builder.element(XmlConsts.text, nest: question.name); - } - }); - // question text - builder.element(XmlConsts.questiontext, nest: () { - builder.attribute(XmlConsts.format, XmlConsts.html); - builder.element(XmlConsts.text, nest: () { - builder.cdata(question.questionText); - }); - }); - // general feedback - if (question.generalFeedback != null) { - builder.element(XmlConsts.generalfeedback, nest: () { - builder.attribute(XmlConsts.format, XmlConsts.html); - builder.element(XmlConsts.text, nest: () { - builder.cdata(question.generalFeedback!); - }); - }); - } - // default grade - if (question.defaultGrade != null) { - builder.element(XmlConsts.defaultgrade, nest: question.defaultGrade); - } - // response format - if (question.responseFormat != null) { - builder.element(XmlConsts.responseformat, nest: question.responseFormat); - } - // response required - if (question.responseRequired != null) { - builder.element(XmlConsts.responserequired, nest: question.responseRequired); - } - // attachments required - if (question.attachmentsRequired != null) { - builder.element(XmlConsts.attachmentsrequired, nest: question.attachmentsRequired); - } - // response template - if (question.responseTemplate != null) { - builder.element(XmlConsts.responsetemplate, nest: () { - builder.cdata(question.responseTemplate!); - }); - } - // grader info - if (question.graderInfo != null) { - builder.element(XmlConsts.graderinfo, nest: () { - builder.attribute(XmlConsts.format, XmlConsts.html); - builder.element(XmlConsts.text, nest: () { - builder.cdata(question.graderInfo!); - }); - }); - } - // answers - for (Answer answer in question.answerList) { - builder.element(XmlConsts.answer, nest: () { - builder.attribute(XmlConsts.fraction, answer.fraction); - builder.element(XmlConsts.text, nest: answer.answerText); - if (answer.feedbackText != null) { - builder.element(XmlConsts.feedback, nest: () { - builder.element(XmlConsts.text, nest: answer.feedbackText); - }); - } - }); - } - }); - } - builder.element(XmlConsts.promptUsed, nest: quiz.promptUsed); - }); - return builder.buildDocument(); - } - - static String getShortenedDesc(String description) { - int charLimit = 30; - String desc = HtmlConverter.convert(description.trim()); - desc = desc.replaceAll('\n', ''); - if (desc.length <= charLimit) return desc; - desc = desc.substring(0, charLimit); - desc = desc.substring(0, desc.lastIndexOf(' ')); - desc = "$desc..."; - return desc; - } - - // Split a quiz into list of smaller quizzes - static List splitQuiz(Quiz quiz) { - int qperq; - switch (quiz.questionList.first.type) { - case 'essay': - qperq = 1; - break; - case 'multichoice': - case 'truefalse': - case 'shortanswer': - default: - qperq = 4; - } - List> splitQuizzes = quiz.questionList.slices(qperq).toList(); - List quizList = []; - for (List q in splitQuizzes) { - quizList.add(Quiz(questionList: q)); - } - return quizList; - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/TestFiles/allThree.xml b/team_a/teamA/lib/TestFiles/allThree.xml deleted file mode 100644 index d311751f..00000000 --- a/team_a/teamA/lib/TestFiles/allThree.xml +++ /dev/null @@ -1,166 +0,0 @@ - - - - - Pythagorean Theorem - - - - - Question 1 - - - The Pythagorean Theorem states that in a right triangle, the square of the length of the hypotenuse is equal to the sum of the squares of the lengths of the other two sides. - - - True - - Correct! This is the definition of the Pythagorean Theorem. - - - - False - - Incorrect. This statement is indeed true. - - - - - - Question 2 - - - In a right triangle, the Pythagorean Theorem can only be applied if the triangle is isosceles. - - - True - - Incorrect. The Pythagorean Theorem applies to all right triangles, not just isosceles ones. - - - - False - - Correct! The theorem applies to any right triangle. - - - - - - Question 3 - - - The lengths of the sides of a right triangle can be represented by the formula a² + b² = c², where c is the hypotenuse. - - - True - - Correct! This is the Pythagorean theorem representation. - - - - False - - Incorrect. This statement accurately describes the theorem. - - - - - - Question 4 - - - The Pythagorean Theorem is applicable only in three-dimensional geometry. - - - True - - Incorrect. The Pythagorean Theorem is specific to two-dimensional right triangles. - - - - False - - Correct! It is applicable only in two dimensions. - - - - - - Question 5 - - - Which of the following is the correct equation according to the Pythagorean Theorem? - - - a² + b² = c² - - Correct! This is the fundamental equation of the Pythagorean Theorem. - - - - a² - b² = c² - - Incorrect. This is not the correct application of the theorem. - - - - c² = a + b - - Incorrect. This does not represent the Pythagorean Theorem. - - - - a + b = c - - Incorrect. This does not describe the relationship between the sides of a right triangle. - - - - - - Question 6 - - - If a right triangle has legs of lengths 3 and 4, what is the length of the hypotenuse? - - - 5 - - Correct! Using the Pythagorean Theorem: 3² + 4² = 9 + 16 = 25, so c = √25 = 5. - - - - 7 - - Incorrect. This does not follow from the theorem. - - - - 6 - - Incorrect. This is not the correct calculation based on the theorem. - - - - 8 - - Incorrect. This does not represent the hypotenuse length. - - - - - - Question 7 - - - What is the Pythagorean Theorem used for in mathematics? - - - To calculate the length of a side in a right triangle - - Correct! The theorem is used to find unknown side lengths in right triangles. - - - - diff --git a/team_a/teamA/lib/TestFiles/multipleChoice.xml b/team_a/teamA/lib/TestFiles/multipleChoice.xml deleted file mode 100644 index 5beff788..00000000 --- a/team_a/teamA/lib/TestFiles/multipleChoice.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - Pythagorean Theorem - - - - Pythagorean Theorem Question 1 - What is the formula for the Pythagorean theorem? - a² + b² = c²Correct! This is the standard formula representing the Pythagorean theorem. - a² - b² = c²Incorrect. This formula is not correct for the Pythagorean theorem. - 2a + b = cIncorrect. This does not represent the Pythagorean theorem. - a + b = cIncorrect. This is not the correct formula for the theorem. - - - - Pythagorean Theorem Question 2 - If one leg of a right triangle is 6 and the other is 8, what is the length of the hypotenuse? - 10Correct! Using the Pythagorean theorem: √(6² + 8²) = √(36 + 64) = √100 = 10. - 12Incorrect. The correct calculation gives a hypotenuse of 10. - 14Incorrect. This does not follow from the Pythagorean theorem. - 8Incorrect. This is one of the leg lengths, not the hypotenuse. - - - - Pythagorean Theorem Question 3 - What type of triangle does the Pythagorean theorem apply to? - Right triangleCorrect! The Pythagorean theorem is specific to right triangles. - Equilateral triangleIncorrect. The theorem does not apply to equilateral triangles. - Isosceles triangleIncorrect. The theorem is not applicable to isosceles triangles unless they are right triangles. - Scalene triangleIncorrect. The theorem is specifically for right triangles. - - - - Pythagorean Theorem Question 4 - If the hypotenuse is 13 and one leg is 5, what is the length of the other leg? - 12Correct! Using the theorem: √(13² - 5²) = √(169 - 25) = √144 = 12. - 10Incorrect. This does not follow the correct calculation. - 8Incorrect. This is not the correct length derived from the theorem. - 6Incorrect. This value does not satisfy the Pythagorean theorem. - - - - Pythagorean Theorem Question 5 - Which of the following is a Pythagorean triple? - 3, 4, 5Correct! 3² + 4² = 5², thus forming a Pythagorean triple. - 5, 12, 13Incorrect. This is also a Pythagorean triple, but not the only one. - 7, 24, 25Incorrect. This is a valid Pythagorean triple as well. - 2, 2, 3Incorrect. This does not form a Pythagorean triple. - - - - Pythagorean Theorem Question 6 - In a right triangle, if the lengths of the legs are equal, what type of triangle is it? - Isosceles right triangleCorrect! An isosceles right triangle has two equal legs and one right angle. - Equilateral triangleIncorrect. An equilateral triangle has all sides equal, not just the legs. - Scalene triangleIncorrect. A scalene triangle has all sides of different lengths. - Obtuse triangleIncorrect. An obtuse triangle has one angle greater than 90 degrees. - - - - Pythagorean Theorem Question 7 - If a triangle has sides of lengths 8, 15, and 17, is it a right triangle? - YesCorrect! 8² + 15² = 17² verifies that it is a right triangle. - NoIncorrect. This combination does form a right triangle. - - - - Pythagorean Theorem Question 8 - What is the relationship of the lengths of the sides in a right triangle? - The square of the hypotenuse is equal to the sum of the squares of the legs.Correct! This defines the Pythagorean theorem. - The sum of all sides is equal to 180 degrees.Incorrect. This refers to the sum of angles in any triangle, not the Pythagorean theorem. - The legs are always equal in length.Incorrect. This is not true for all right triangles. - Each angle must be acute.Incorrect. A right triangle has one right angle. - - - - Pythagorean Theorem Question 9 - Can the Pythagorean theorem be used in three-dimensional geometry? - No, it only applies to two-dimensional triangles.Incorrect. It applies specifically to two-dimensional right triangles. - Yes, but in a modified form.Correct! The theorem can be adapted for three-dimensional cases using the distance formula. - - - - Pythagorean Theorem Question 10 - What do we call a triangle that satisfies the Pythagorean theorem? - Pythagorean triangleCorrect! Triangles that satisfy this relationship are known as Pythagorean triangles. - Right triangleIncorrect. While all Pythagorean triangles are right triangles, not all right triangles are referred to specifically as Pythagorean triangles. - Acute triangleIncorrect. An acute triangle has all angles less than 90 degrees, which does not satisfy the theorem. - Obtuse triangleIncorrect. An obtuse triangle has one angle greater than 90 degrees, which does not satisfy the theorem. - - diff --git a/team_a/teamA/lib/TestFiles/shortAnswer.xml b/team_a/teamA/lib/TestFiles/shortAnswer.xml deleted file mode 100644 index 81adce24..00000000 --- a/team_a/teamA/lib/TestFiles/shortAnswer.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - Pythagorean Theorem - - - - Pythagorean Theorem Question 1 - What is the length of the hypotenuse of a right triangle if the lengths of the two legs are 9 and 12? - 15 - √225 - - - - Pythagorean Theorem Question 2 - If the hypotenuse of a right triangle is 10 and one leg is 6, what is the length of the other leg? - 8 - √(10² - 6²) - - - - Pythagorean Theorem Question 3 - Provide an example of a Pythagorean triple. - 3, 4, 5 - 5, 12, 13 - 7, 24, 25 - - diff --git a/team_a/teamA/lib/TestFiles/trueFalse.xml b/team_a/teamA/lib/TestFiles/trueFalse.xml deleted file mode 100644 index dd7fefbb..00000000 --- a/team_a/teamA/lib/TestFiles/trueFalse.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - - Pythagorean Theorem - - - - Pythagorean Theorem Question 1 - The Pythagorean theorem states that in a right triangle, the square of the hypotenuse is equal to the sum of the squares of the other two sides. - TrueCorrect! This is the fundamental statement of the Pythagorean theorem. - FalseIncorrect. The Pythagorean theorem indeed states that the square of the hypotenuse is equal to the sum of the squares of the other two sides. - - - - Pythagorean Theorem Question 2 - For a right triangle with legs of lengths 3 and 4, the length of the hypotenuse is 5. - TrueCorrect! Using the Pythagorean theorem: 3² + 4² = 9 + 16 = 25, so the hypotenuse is √25 = 5. - FalseIncorrect. The hypotenuse is indeed 5, as verified by the Pythagorean theorem. - - - - Pythagorean Theorem Question 3 - The Pythagorean theorem can only be used for triangles with one right angle. - TrueCorrect! The Pythagorean theorem applies specifically to right triangles. - FalseIncorrect. The theorem is specifically applicable only to right triangles. - - - - Pythagorean Theorem Question 4 - The formula a² + b² = c² can be rearranged to find any side of a right triangle. - TrueCorrect! You can rearrange the formula to solve for a, b, or c as needed. - FalseIncorrect. The formula can indeed be rearranged to solve for any side of the triangle. - - - - Pythagorean Theorem Question 5 - The lengths of the sides of a right triangle must always be integers. - TrueIncorrect. While there are integer solutions (like 3, 4, 5), sides can also be non-integers. - FalseCorrect! The sides of a right triangle can be any real numbers, not just integers. - - - - Pythagorean Theorem Question 6 - If a triangle has sides of lengths 5, 12, and 13, it is a right triangle. - TrueCorrect! 5² + 12² = 25 + 144 = 169, which is 13², confirming it is a right triangle. - FalseIncorrect. The triangle with these side lengths is indeed a right triangle by the Pythagorean theorem. - - - - Pythagorean Theorem Question 7 - The hypotenuse is always the longest side in a right triangle. - TrueCorrect! By definition, the hypotenuse is the longest side of a right triangle. - FalseIncorrect. The hypotenuse is always the longest side in a right triangle. - - - - Pythagorean Theorem Question 8 - The Pythagorean theorem can be extended to non-right triangles. - TrueIncorrect. The Pythagorean theorem specifically applies to right triangles only. - FalseCorrect! The theorem is not applicable to non-right triangles. - - - - Pythagorean Theorem Question 9 - The Pythagorean theorem can be used to derive the distance formula in coordinate geometry. - TrueCorrect! The distance formula is derived from the Pythagorean theorem in the context of the coordinate plane. - FalseIncorrect. The distance formula is indeed based on the Pythagorean theorem. - - - - Pythagorean Theorem Question 10 - If one leg of a right triangle is 8 and the hypotenuse is 10, the length of the other leg can be found using the Pythagorean theorem. - TrueCorrect! You can find the other leg's length by using the formula: 10² - 8² = c². - FalseIncorrect. The other leg's length can be calculated using the Pythagorean theorem. - - \ No newline at end of file diff --git a/team_a/teamA/lib/Views/about_page.dart b/team_a/teamA/lib/Views/about_page.dart deleted file mode 100644 index c7f9822b..00000000 --- a/team_a/teamA/lib/Views/about_page.dart +++ /dev/null @@ -1,70 +0,0 @@ -import "package:flutter/material.dart"; -import "package:learninglens_app/Api/lms/factory/lms_factory.dart"; -import "package:learninglens_app/Controller/custom_appbar.dart"; - -/// -/// Template for views -/// Need to change the class name to the name based on the view you -/// are creating to include the constructor as well. -/// Need to change the class that extends State as well. Make sure -/// that the createState function returns the same name. -/// -/// The CustomAppBar is already included, but you will need to update -/// the title string. -/// -/// The content for the page begins in the children array. There is -/// a single text box as a place holder. -/// -/// To add your view to the rest of the app, you will have to add it -/// to the dashboard.dart file. On line ~213, there is a List called -/// buttonData. You will need to update the 'onPressed' section with -/// your new template. You can see examples that Derek has already -/// done on other buttons. -/// -class AboutPage extends StatefulWidget{ - AboutPage(); - - @override - State createState(){ - return _TemplateState(); - } -} - -class _TemplateState extends State{ - - @override - Widget build(BuildContext context){ - return Scaffold( - appBar: CustomAppBar( - title: 'About Learning Lens', - onRefresh: () { - // Add refresh logic here - }, - userprofileurl: LmsFactory.getLmsService().profileImage ?? '' - ), - body: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children:[ - // Add Content here - Padding( - padding: EdgeInsets.all(16.0), // Adjust padding as needed - child: Text( - 'Learning Lens v2.0 by Team EduLense', - style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold), - textAlign: TextAlign.left, - ), - ), - 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', - style: TextStyle(fontSize: 15), - ), - ), - ] - ) - ) - ); - } -} diff --git a/team_a/teamA/lib/Views/analytics_page.dart b/team_a/teamA/lib/Views/analytics_page.dart deleted file mode 100644 index 94af9bb1..00000000 --- a/team_a/teamA/lib/Views/analytics_page.dart +++ /dev/null @@ -1,1280 +0,0 @@ -import 'dart:io' show File; // For non-web file I/O -import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:flutter/material.dart'; -import 'package:file_picker/file_picker.dart'; // For file saving on non-web platforms -import 'package:learninglens_app/beans/question_stat_type.dart'; -import 'package:learninglens_app/stub/html_stub.dart' - if (dart.library.html) 'dart:html' as html; - -import 'package:pdf/widgets.dart' as pw; // PDF package -import 'package:excel/excel.dart'; // Excel package - -// Import the LMS services using prefixes so that type checks work correctly. -import 'package:learninglens_app/Api/lms/moodle/moodle_lms_service.dart' - as moodle; -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/beans/assignment.dart'; -import 'package:learninglens_app/beans/participant.dart'; -import 'package:learninglens_app/beans/quiz.dart'; -import 'package:learninglens_app/beans/quiz_type.dart'; - -import 'package:learninglens_app/Views/dashboard.dart'; -import 'package:learninglens_app/Views/user_settings.dart'; - -// Import the APIs for the Learning Lens Model (LLM). -import 'package:learninglens_app/Api/llm/enum/llm_enum.dart'; -import 'package:learninglens_app/Api/llm/openai_api.dart'; -import 'package:learninglens_app/Api/llm/grok_api.dart'; -import 'package:learninglens_app/Api/llm/perplexity_api.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; -import 'dart:convert'; - -/// Enum to represent export formats. -enum ExportFormat { pdf, excel } - -/// AnalyticsPage displays the Analytics Dashboard where teachers can: -/// - View overall analytics data (live data fetched from the LMS) -/// - Generate a detailed report for essay assignments only (quizzes are omitted) -/// - Export the generated report as a valid PDF or Excel file -/// (using proper PDF/Excel libraries) -/// - View tables in fixed-height containers with visible scrollbars. -/// -/// When a student is clicked in the breakdown table, a detail panel appears -/// on the right showing that student's assignment details (non-editable). -/// -/// A simple wrapper to hold either an essay assignment or a quiz. -/// The `type` property distinguishes between the two. -class Assessment { - final dynamic assessment; // Either an Assignment (essay) or a Quiz. - final String type; // "essay" or "quiz" - - Assessment({required this.assessment, required this.type}); - - String get name { - if (type == "essay") { - return (assessment as Assignment).name; - } else { - return (assessment as Quiz).name ?? 'Unknown Quiz'; - } - } - - int get id { - if (type == "essay") { - return (assessment as Assignment).id ?? 0; - } else { - return (assessment as Quiz).id ?? 0; - } - } -} - -class AnalyticsPage extends StatefulWidget { - const AnalyticsPage({Key? key}) : super(key: key); - - @override - _AnalyticsPageState createState() => _AnalyticsPageState(); -} - -class _AnalyticsPageState extends State { - final lmsService = LmsFactory.getLmsService(); - // Live analytics data fetched from the LMS. - Map? analyticsData; - bool isLoading = false; - String errorMsg = ''; - - // Live data for dropdowns. - List _coursesData = []; - List _assessmentsData = []; - List _participantsData = []; - - // Selections from dropdowns. - Course? _selectedCourse; - String? _selectedSubject; - Assessment? _selectedAssessment; - - // Student breakdown report built from live LMS participant data. - List> _studentBreakdown = []; - Map? _selectedStudent; - - // For quiz assessments, question breakdown data. - List _questionBreakdown = []; - - // Scroll controllers for tables. - late ScrollController _verticalStudentController; - late ScrollController _horizontalStudentController; - late ScrollController _verticalQuestionController; - late ScrollController _horizontalQuestionController; - - // AI Analysis data. - List> _aiAnalysisData = []; - bool _isAnalyzingAI = false; - - @override - void initState() { - super.initState(); - _verticalStudentController = ScrollController(); - _horizontalStudentController = ScrollController(); - _verticalQuestionController = ScrollController(); - _horizontalQuestionController = ScrollController(); - _fetchAnalyticsData(); - } - - @override - void dispose() { - _verticalStudentController.dispose(); - _horizontalStudentController.dispose(); - _verticalQuestionController.dispose(); - _horizontalQuestionController.dispose(); - super.dispose(); - } - - // --------------------------------------------------------------------------- - // _fetchAnalyticsData: - // Fetches live data from the LMS (courses, initial quizzes/essays). - // --------------------------------------------------------------------------- - Future _fetchAnalyticsData() async { - setState(() { - isLoading = true; - errorMsg = ''; - }); - try { - _coursesData = await lmsService.getUserCourses(); - int totalCourses = _coursesData.length; - if (_coursesData.isNotEmpty) { - _selectedCourse = _coursesData.first; - _selectedSubject = _selectedCourse!.subject ?? "General"; - // Fetch essays. - List essayList = - await lmsService.getEssays(_selectedCourse!.id); - // Fetch quizzes (if available). - List quizList = []; - try { - quizList = await (lmsService as moodle.MoodleLmsService) - .getQuizzes(_selectedCourse!.id); - } catch (e) { - print("getQuizzes not available or failed: $e"); - } - // Combine them into one list - _assessmentsData = [ - ...essayList.map((a) => Assessment(assessment: a, type: "essay")), - ...quizList.map((q) => Assessment(assessment: q, type: "quiz")) - ]; - if (_assessmentsData.isNotEmpty) { - _selectedAssessment = _assessmentsData.first; - } - } - setState(() { - analyticsData = { - 'source': lmsService is moodle.MoodleLmsService - ? 'Moodle' - : 'Google Classroom', - 'totalCourses': totalCourses, - 'studentPerformance': 'Live Performance Data', - 'iepProgress': 'Live IEP Data', - 'courseEngagement': 'Live Engagement Metrics', - }; - }); - } catch (e) { - setState(() { - errorMsg = 'Failed to load analytics data: $e'; - }); - } finally { - setState(() { - isLoading = false; - }); - } - } - - // --------------------------------------------------------------------------- - // _generateReport: - // Builds the student breakdown for the currently selected assessment only - // (i.e., one quiz or essay). - // --------------------------------------------------------------------------- - Future _generateReport() async { - if (_selectedCourse == null) return; - setState(() { - isLoading = true; - errorMsg = ''; - _studentBreakdown.clear(); - _questionBreakdown.clear(); - _selectedStudent = null; - }); - - try { - if (isQuiz()) { - // Grab participants for this quiz. - int quizId = _selectedAssessment!.assessment.id; - _participantsData = await (lmsService as moodle.MoodleLmsService) - .getQuizGradesForParticipants( - _selectedCourse!.id.toString(), quizId); - // Filter out non-students, if needed. - _participantsData = _participantsData - .where((i) => i.roles.contains('student')) - .toList(); - } else if (isEssay()) { - // Grab participants for this essay. - int assignmentId = _selectedAssessment!.assessment.id; - _participantsData = await (lmsService as moodle.MoodleLmsService) - .getEssayGradesForParticipants( - _selectedCourse!.id.toString(), assignmentId); - } else { - throw Exception("Unsupported Assessment Type"); - } - - // Build the table shown in the "Student Breakdown" section. - getStudentBreakdown(_participantsData); - - // If it's a quiz, also fetch the question breakdown. - await _fetchQuestionBreakdown(); - } catch (e) { - setState(() { - errorMsg = 'Failed to generate report: $e'; - }); - } finally { - setState(() { - isLoading = false; - }); - } - } - - // --------------------------------------------------------------------------- - // getStudentBreakdown: - // Builds `_studentBreakdown` from the given participant list for the currently - // selected assessment only. - // --------------------------------------------------------------------------- - void getStudentBreakdown(List participantsData) { - _studentBreakdown = participantsData.map((participant) { - double? grade = participant.avgGrade; - String displayGrade = (grade != null) ? '${grade.toInt()}%' : '0%'; - return { - 'id': participant.id, - 'studentName': participant.fullname, - 'avgGrade': displayGrade, - 'classRank': 0, - }; - }).toList(); - - // Sort descending by numeric grade. - _studentBreakdown.sort((a, b) { - int aGrade = int.tryParse(a['avgGrade'].replaceAll('%', '')) ?? 0; - int bGrade = int.tryParse(b['avgGrade'].replaceAll('%', '')) ?? 0; - return bGrade.compareTo(aGrade); - }); - - // Assign a 1-based rank. - for (int i = 0; i < _studentBreakdown.length; i++) { - _studentBreakdown[i]['classRank'] = i + 1; - } - } - - // --------------------------------------------------------------------------- - // _saveReport: - // Exports the generated report as PDF or Excel (only for the single selected assessment). - // --------------------------------------------------------------------------- - Future _saveReport() async { - final format = await _chooseExportFormat(); - if (format == null) return; - String extension = (format == ExportFormat.pdf) ? 'pdf' : 'xlsx'; - String defaultName = 'my_report.$extension'; - - if (kIsWeb) { - // Build bytes. - List bytes = (format == ExportFormat.pdf) - ? await _exportReportAsPdf() - : await _exportReportAsExcel(); - final blob = html.Blob([bytes]); - final url = html.Url.createObjectUrlFromBlob(blob); - final anchor = html.AnchorElement(href: url) - ..style.display = 'none' - ..download = defaultName; - html.document.body?.append(anchor); - anchor.click(); - anchor.remove(); - html.Url.revokeObjectUrl(url); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text('Report exported as $extension via browser download.')), - ); - } else { - final savePath = await _pickFileLocation(defaultName); - if (savePath == null) return; - try { - List bytes = (format == ExportFormat.pdf) - ? await _exportReportAsPdf() - : await _exportReportAsExcel(); - final file = File(savePath); - await file.writeAsBytes(bytes); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Report saved as $extension at:\n$savePath')), - ); - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to save report: $e')), - ); - } - } - } - - // --------------------------------------------------------------------------- - // _chooseExportFormat: - // Prompts the user to select whether to export the report as PDF or Excel. - // --------------------------------------------------------------------------- - Future _chooseExportFormat() async { - return showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Select Export Format'), - content: const Text( - 'Would you like to export the report as PDF or Excel?'), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context, ExportFormat.pdf), - child: const Text('PDF'), - ), - TextButton( - onPressed: () => Navigator.pop(context, ExportFormat.excel), - child: const Text('Excel'), - ), - TextButton( - onPressed: () => Navigator.pop(context, null), - child: const Text('Cancel'), - ), - ], - ); - }, - ); - } - - // --------------------------------------------------------------------------- - // _exportReportAsPdf: - // Uses the pdf package to generate a PDF document containing the student breakdown - // and, if applicable, the question breakdown. - // --------------------------------------------------------------------------- - Future> _exportReportAsPdf() async { - final pdf = pw.Document(); - pdf.addPage( - pw.MultiPage( - build: (pw.Context context) { - List widgets = []; - - // Dynamic export for student breakdown. - if (_studentBreakdown.isNotEmpty) { - // Dynamically capture all keys as headers. - final studentHeaders = _studentBreakdown.first.keys.toList(); - // Map each student to a list of string values. - final studentData = _studentBreakdown - .map((student) => - student.values.map((value) => value.toString()).toList()) - .toList(); - widgets.add(pw.Header( - level: 0, child: pw.Text("Student Breakdown Report"))); - widgets.add( - pw.Table.fromTextArray( - headers: studentHeaders, - data: studentData, - ), - ); - } - - // Export question breakdown (only for quizzes). - if (isQuiz() && _questionBreakdown.isNotEmpty) { - widgets.add(pw.SizedBox(height: 20)); - widgets - .add(pw.Header(level: 0, child: pw.Text("Question Breakdown"))); - widgets.add( - pw.Table.fromTextArray( - headers: [ - 'Q#', - 'Question Type', - 'Question', - '% Answered Correct', - '# Correct', - '# Incorrect', - '# Total Attempts' - ], - data: _questionBreakdown - .map((q) => [ - q.id.toString(), - q.questionType, - q.questionText, - "${computePercentCorrect(q).toStringAsFixed(2)}%", - q.numCorrect.toString(), - q.numIncorrect.toString(), - q.totalAttempts.toString(), - ]) - .toList(), - ), - ); - } - return widgets; - }, - ), - ); - return pdf.save(); - } - - // --------------------------------------------------------------------------- - // _exportReportAsExcel: - // Uses the excel package to generate an Excel file containing the student breakdown - // and, if applicable, the question breakdown. - // --------------------------------------------------------------------------- - Future> _exportReportAsExcel() async { - var excel = Excel.createExcel(); - - // Dynamic export for student breakdown. - Sheet studentSheet = excel['Student Breakdown']; - if (_studentBreakdown.isNotEmpty) { - // Get headers dynamically from the first map. - var studentHeaders = _studentBreakdown.first.keys.toList(); - studentSheet.appendRow(studentHeaders); - // Append each student row by mapping the values to strings. - for (var student in _studentBreakdown) { - studentSheet.appendRow( - student.values.map((value) => value.toString()).toList()); - } - } - - // Export question breakdown if it's a quiz. - if (isQuiz() && _questionBreakdown.isNotEmpty) { - Sheet questionSheet = excel['Question Breakdown']; - questionSheet.appendRow([ - 'Q#', - 'Question Type', - 'Question', - '% Answered Correct', - '# Correct', - '# Incorrect', - '# Total Attempts' - ]); - for (var q in _questionBreakdown) { - questionSheet.appendRow([ - q.id, - q.questionType, - q.questionText, - "${computePercentCorrect(q).toStringAsFixed(2)}%", - q.numCorrect, - q.numIncorrect, - q.totalAttempts, - ]); - } - } - - return excel.encode()!; - } - - // --------------------------------------------------------------------------- - // _pickFileLocation: - // For non-web platforms, uses FilePicker to let the user choose a save location. - // On web, file saving is handled via an AnchorElement. - // --------------------------------------------------------------------------- - Future _pickFileLocation(String defaultName) async { - if (kIsWeb) return null; - final result = await FilePicker.platform.saveFile( - dialogTitle: 'Save Report', - fileName: defaultName, - ); - return result; - } - - // --------------------------------------------------------------------------- - // Export AI Analysis Functions - // --------------------------------------------------------------------------- - Future _exportAIAnalysis() async { - final format = await _chooseExportFormat(); - if (format == null) return; - String extension = (format == ExportFormat.pdf) ? 'pdf' : 'xlsx'; - String defaultName = 'ai_analysis_report.$extension'; - - if (kIsWeb) { - List bytes = (format == ExportFormat.pdf) - ? await _exportAIAnalysisAsPdf() - : await _exportAIAnalysisAsExcel(); - final blob = html.Blob([bytes]); - final url = html.Url.createObjectUrlFromBlob(blob); - final anchor = html.AnchorElement(href: url) - ..style.display = 'none' - ..download = defaultName; - html.document.body?.append(anchor); - anchor.click(); - anchor.remove(); - html.Url.revokeObjectUrl(url); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'AI Analysis exported as $extension via browser download.')), - ); - } else { - final savePath = await _pickFileLocation(defaultName); - if (savePath == null) return; - try { - List bytes = (format == ExportFormat.pdf) - ? await _exportAIAnalysisAsPdf() - : await _exportAIAnalysisAsExcel(); - final file = File(savePath); - await file.writeAsBytes(bytes); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('AI Analysis saved as $extension at:\n$savePath')), - ); - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to save AI Analysis: $e')), - ); - } - } - } - - Future> _exportAIAnalysisAsPdf() async { - final pdf = pw.Document(); - pdf.addPage( - pw.MultiPage( - build: (pw.Context context) { - if (_aiAnalysisData.isEmpty) { - return [ - pw.Center(child: pw.Text("No AI Analysis Data available.")) - ]; - } - final headers = ['Student', 'Status', 'Comments']; - final data = _aiAnalysisData - .map((row) => [ - row['Student']?.toString() ?? '', - row['Status']?.toString() ?? '', - row['Comments']?.toString() ?? '', - ]) - .toList(); - return [ - pw.Header(level: 0, child: pw.Text("AI Analysis Summary")), - pw.Table.fromTextArray(headers: headers, data: data), - ]; - }, - ), - ); - return pdf.save(); - } - - Future> _exportAIAnalysisAsExcel() async { - var excel = Excel.createExcel(); - Sheet sheet = excel['AI Analysis']; - sheet.appendRow(['Student', 'Status', 'Comments']); - for (var row in _aiAnalysisData) { - sheet.appendRow([ - row['Student']?.toString() ?? '', - row['Status']?.toString() ?? '', - row['Comments']?.toString() ?? '', - ]); - } - return excel.encode()!; - } - - // --------------------------------------------------------------------------- - // _buildReportForm: - // Displays dropdowns for selecting course, subject, and assessment, - // along with Generate and Export buttons. - // --------------------------------------------------------------------------- - Widget _buildReportForm() { - return Container( - width: 100, - padding: const EdgeInsets.all(16), - color: Colors.grey[200], - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Generate New Report', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), - const SizedBox(height: 8), - // Course dropdown. - DropdownButtonFormField( - value: _selectedCourse, - decoration: const InputDecoration(labelText: 'Course'), - items: _coursesData.map((course) { - return DropdownMenuItem( - value: course, - child: Text(course.fullName.isNotEmpty - ? course.fullName - : course.shortName), - ); - }).toList(), - onChanged: (val) async { - setState(() { - _selectedCourse = val; - _selectedSubject = val?.subject ?? "General"; - // Clear dependent tables when course changes. - _studentBreakdown.clear(); - _questionBreakdown.clear(); - _aiAnalysisData.clear(); - _selectedStudent = null; - }); - if (_selectedCourse != null) { - // Fetch essays and quizzes, then combine them. - List essays = - await lmsService.getEssays(_selectedCourse!.id); - List quizzes = []; - try { - quizzes = await (lmsService as dynamic) - .getQuizzes(_selectedCourse!.id); - } catch (e) { - print("getQuizzes not available or failed: $e"); - } - _assessmentsData = [ - ...essays - .map((a) => Assessment(assessment: a, type: "essay")), - ...quizzes.map((q) => Assessment(assessment: q, type: "quiz")) - ]; - if (_assessmentsData.isNotEmpty) { - _selectedAssessment = _assessmentsData.first; - } - setState(() {}); - } - }, - ), - // Subject dropdown. - if (_selectedCourse != null) - DropdownButtonFormField( - value: _selectedSubject, - decoration: const InputDecoration(labelText: 'Subject'), - items: [ - DropdownMenuItem( - value: _selectedSubject, - child: Text(_selectedSubject ?? "General"), - ) - ], - onChanged: (val) { - setState(() { - _selectedSubject = val; - // Optionally clear dependent tables if needed. - _studentBreakdown.clear(); - _questionBreakdown.clear(); - _aiAnalysisData.clear(); - _selectedStudent = null; - }); - }, - ), - // Assessment dropdown. - DropdownButtonFormField( - value: _selectedAssessment, - decoration: const InputDecoration(labelText: 'Assessment'), - items: _assessmentsData.map((assessment) { - return DropdownMenuItem( - value: assessment, - child: Text( - '${assessment.name} (${assessment.type.toUpperCase()})'), - ); - }).toList(), - onChanged: (val) { - setState(() { - _selectedAssessment = val; - // Clear question breakdown, student breakdown and AI analysis when assessment changes. - _questionBreakdown.clear(); - _studentBreakdown.clear(); - _aiAnalysisData.clear(); - _selectedStudent = null; - }); - }, - ), - const SizedBox(height: 12), - Row( - children: [ - ElevatedButton( - onPressed: _generateReport, - child: const Text('Generate'), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: _studentBreakdown.isNotEmpty ? _saveReport : null, - child: const Text('Export'), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: _studentBreakdown.isNotEmpty ? _analyzeReport : null, - child: _isAnalyzingAI - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('AI Analyze'), - ), - ], - ), - ], - ), - ); - } - - // --------------------------------------------------------------------------- - // _buildQuestionBreakdown: - // Displays the question breakdown table for quiz assessments. - // This is shown only when a quiz is selected. - // Here, each DataCell wraps its Text widget with an IntrinsicWidth widget - // to ensure that the column width adjusts to the largest text. - // Additionally, the entire table is wrapped in a FittedBox to scale down - // the table if the user has a smaller screen or they change their resolution. - // --------------------------------------------------------------------------- - Widget _buildQuestionBreakdown() { - if (isEssay()) { - return const SizedBox.shrink(); - } - if (_questionBreakdown.isEmpty) { - return const Center(child: Text('No question breakdown available.')); - } - return SizedBox( - height: 200, - child: Scrollbar( - thumbVisibility: true, - controller: _verticalQuestionController, - child: SingleChildScrollView( - controller: _verticalQuestionController, - scrollDirection: Axis.vertical, - child: Scrollbar( - thumbVisibility: true, - controller: _horizontalQuestionController, - notificationPredicate: (notification) => notification.depth == 2, - child: SingleChildScrollView( - controller: _horizontalQuestionController, - scrollDirection: Axis.horizontal, - // This is where the magic happens with the FittedBox and DataTable. - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: DataTable( - columnSpacing: 12.0, - columns: const [ - DataColumn(label: Text('#')), - DataColumn(label: Text('Question Type')), - DataColumn(label: Text('Question')), - DataColumn(label: Text('% Answered Correct')), - DataColumn(label: Text('# Correct')), - DataColumn(label: Text('# Incorrect')), - DataColumn(label: Text('# Total Attempts')), - ], - rows: _questionBreakdown.map((q) { - return DataRow( - cells: [ - DataCell(IntrinsicWidth(child: Text(q.id.toString()))), - DataCell(IntrinsicWidth(child: Text(q.questionType))), - DataCell(IntrinsicWidth(child: Text(q.questionText))), - DataCell(IntrinsicWidth( - child: Text( - "${computePercentCorrect(q).toStringAsFixed(2)}%"))), - DataCell(IntrinsicWidth( - child: Text(q.numCorrect.toString()))), - DataCell(IntrinsicWidth( - child: Text(q.numIncorrect.toString()))), - DataCell(IntrinsicWidth( - child: Text(q.totalAttempts.toString()))), - ], - ); - }).toList(), - ), - ), - ), - ), - ), - ), - ); - } - - // --------------------------------------------------------------------------- - // _buildStudentTable: - // Returns ONLY the table of student data. The detail panel is separate. - // --------------------------------------------------------------------------- - Widget _buildStudentTable() { - if (_studentBreakdown.isEmpty && !isLoading) { - return const Center(child: Text('No student breakdown available.')); - } - if (isLoading) { - return const Center(child: CircularProgressIndicator()); - } - return SizedBox( - height: 300, - child: Scrollbar( - thumbVisibility: true, - controller: _verticalStudentController, - child: SingleChildScrollView( - controller: _verticalStudentController, - scrollDirection: Axis.vertical, - child: Scrollbar( - thumbVisibility: true, - controller: _horizontalStudentController, - notificationPredicate: (notification) => notification.depth == 2, - child: SingleChildScrollView( - controller: _horizontalStudentController, - scrollDirection: Axis.horizontal, - child: DataTable( - columnSpacing: 12.0, - columns: const [ - DataColumn(label: Text('Student Name')), - DataColumn(label: Text('Average Grade')), - DataColumn(label: Text('Class Rank')), - ], - rows: _studentBreakdown.map((student) { - return DataRow( - cells: [ - DataCell( - InkWell( - onTap: () { - setState(() { - _selectedStudent = student; - }); - }, - child: Text( - student['studentName'].toString(), - style: const TextStyle( - color: Colors.blue, - decoration: TextDecoration.underline, - ), - ), - ), - ), - DataCell(Text(student['avgGrade'].toString())), - DataCell(Text(student['classRank'].toString())), - ], - ); - }).toList(), - ), - ), - ), - ), - ), - ); - } - - // --------------------------------------------------------------------------- - // _buildStudentDetail: - // Displays the selected student's detail info in the bottom-right quadrant. - // --------------------------------------------------------------------------- - Widget _buildStudentDetail() { - if (_selectedStudent == null) { - return const Center( - child: Text( - 'Select a student to see detailed grades.', - style: TextStyle(fontStyle: FontStyle.italic), - ), - ); - } - int studentId = _selectedStudent!['id']; - return FutureBuilder>>( - future: _fetchAllAssessmentsForStudent(studentId), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - if (snapshot.hasError) { - return Center(child: Text('Error loading grades: ${snapshot.error}')); - } - if (!snapshot.hasData || snapshot.data!.isEmpty) { - return Text('No data available for student $studentId.'); - } - List> detailData = snapshot.data!; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Details for ${_selectedStudent!['studentName']}', - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 10), - ...detailData.map((item) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - "${item['Assessment']} (${item['Type']})", - style: const TextStyle(fontSize: 16), - overflow: TextOverflow.ellipsis, - ), - ), - Text( - item['Grade']!, - style: const TextStyle(fontSize: 16), - ), - ], - ), - ); - }).toList(), - ], - ); - }, - ); - } - - // --------------------------------------------------------------------------- - // _fetchAllAssessmentsForStudent: - // Helper function to fetch ALL assessments (quiz or essay) for ONE student. - // --------------------------------------------------------------------------- - Future>> _fetchAllAssessmentsForStudent( - int studentId) async { - if (_selectedCourse == null) return []; - List>> futureList = []; - for (var assessment in _assessmentsData) { - futureList.add(() async { - String gradeStr = "0%"; - if (assessment.type == "quiz") { - final participants = await (lmsService as moodle.MoodleLmsService) - .getQuizGradesForParticipants( - _selectedCourse!.id.toString(), - assessment.id, - ); - final participant = participants.firstWhere( - (p) => p.id == studentId, - orElse: () => Participant.empty(), - ); - if (participant.avgGrade != null) { - gradeStr = "${participant.avgGrade!.toInt()}%"; - } - } else if (assessment.type == "essay") { - final participants = await (lmsService as moodle.MoodleLmsService) - .getEssayGradesForParticipants( - _selectedCourse!.id.toString(), - assessment.id, - ); - final participant = participants.firstWhere( - (p) => p.id == studentId, - orElse: () => Participant.empty(), - ); - if (participant.avgGrade != null) { - gradeStr = "${participant.avgGrade!.toInt()}%"; - } - } - return { - 'Assessment': assessment.name, - 'Type': assessment.type.toUpperCase(), - 'Grade': gradeStr, - }; - }()); - } - return Future.wait(futureList); - } - - // --------------------------------------------------------------------------- - // _fetchQuestionBreakdown: - // If a quiz is selected, fetch its question breakdown. - // --------------------------------------------------------------------------- - Future _fetchQuestionBreakdown() async { - if (isQuiz()) { - try { - int quizId = _selectedAssessment!.assessment.id; - _questionBreakdown = - await (lmsService as dynamic).getQuestionStatsFromQuiz(quizId); - setState(() {}); - } catch (e) { - print("Failed to fetch question breakdown: $e"); - } - } - } - - bool isQuiz() { - return _selectedAssessment != null && _selectedAssessment!.type == "quiz"; - } - - bool isEssay() { - return _selectedAssessment != null && _selectedAssessment!.type == "essay"; - } - - // --------------------------------------------------------------------------- - // _buildMainGrid: - // Creates a 2×2 grid layout: - // Top-left: Report form - // Bottom-left: Student breakdown table - // Top-right: Question breakdown - // Bottom-right: Selected student detail - // --------------------------------------------------------------------------- - Widget _buildMainGrid() { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Left Column (narrow) - Expanded( - flex: 1, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Top-left: the report form - _buildReportForm(), - const SizedBox(height: 20), - const Text( - 'Student Breakdown', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - const SizedBox(height: 8), - Container( - color: Colors.white, - padding: const EdgeInsets.all(8), - child: _buildStudentTable(), - ), - ], - ), - ), - const SizedBox(width: 16), - // Right Column (wider) - Expanded( - flex: 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Top-right: label + question breakdown - const Text( - 'Question Breakdown', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - const SizedBox(height: 8), - Container( - color: Colors.white, - padding: const EdgeInsets.all(8), - child: _buildQuestionBreakdown(), - ), - const SizedBox(height: 20), - // Bottom-right: label + selected student detail - const Text( - 'Student Detail', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - const SizedBox(height: 8), - Container( - color: Colors.white, - padding: const EdgeInsets.all(8), - child: _buildStudentDetail(), - ), - ], - ), - ), - ], - ); - } - - // --------------------------------------------------------------------------- - // _buildContent: - // Builds the overall page content including analytics summary and the 2x2 grid. - // --------------------------------------------------------------------------- - Widget _buildContent() { - if (isLoading && analyticsData == null && errorMsg.isEmpty) { - return const Center(child: CircularProgressIndicator()); - } - if (errorMsg.isNotEmpty) { - return Center(child: Text(errorMsg)); - } - if (analyticsData == null) { - return const Center(child: Text('No analytics data available yet.')); - } - return SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 16), - // Analytics summary. - Center( - child: Column( - children: [ - Text('Analytics Source: ${analyticsData!['source']}', - style: const TextStyle(fontSize: 16)), - Text('Total Courses: ${analyticsData!['totalCourses']}', - style: const TextStyle(fontSize: 16)), - Text( - 'Student Performance: ${analyticsData!['studentPerformance']}', - style: const TextStyle(fontSize: 16)), - Text('IEP Progress: ${analyticsData!['iepProgress']}', - style: const TextStyle(fontSize: 16)), - Text('Course Engagement: ${analyticsData!['courseEngagement']}', - style: const TextStyle(fontSize: 16)), - ], - ), - ), - const SizedBox(height: 20), - // 2x2 grid view. - _buildMainGrid(), - // AI Analysis table below the grid with an export button. - if (_aiAnalysisData.isNotEmpty) ...[ - const SizedBox(height: 30), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'AI Analysis Summary', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - ElevatedButton( - onPressed: _exportAIAnalysis, - child: const Text('Export AI Analysis'), - ), - ], - ), - const SizedBox(height: 10), - _buildAIAnalysisTable(), - ], - const SizedBox(height: 30), - ], - ), - ); - } - - // --------------------------------------------------------------------------- - // build: - // Sets up the Scaffold using the shared CustomAppBar and the main content. - // --------------------------------------------------------------------------- - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, - appBar: CustomAppBar( - title: 'Analytics Dashboard', - userprofileurl: lmsService.profileImage ?? '', - onRefresh: _fetchAnalyticsData, - ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: _buildContent(), - ), - ); - } - - // --------------------------------------------------------------------------- - // build: - // Builds the overall AI analyisis summary below the 2x2 grid. - // --------------------------------------------------------------------------- - Widget _buildAIAnalysisTable() { - if (_aiAnalysisData.isEmpty) return const SizedBox.shrink(); - return DataTable( - columns: const [ - DataColumn(label: Text('Student')), - DataColumn(label: Text('Status')), - DataColumn(label: Text('Comments')), - ], - rows: _aiAnalysisData.map((row) { - return DataRow( - cells: [ - DataCell(Text(row['Student']?.toString() ?? '')), - DataCell(Text(row['Status']?.toString() ?? '')), - DataCell(Text(row['Comments']?.toString() ?? '')), - ], - ); - }).toList(), - ); - } - - /// Computes the percentage a question is answered correctly. - double computePercentCorrect(QuestionStatsType q) { - if (q.totalAttempts == 0) return 0.0; - return ((q.numCorrect + q.numPartial) / q.totalAttempts) * 100; - } - - /// Computes the average grade across the student breakdown. - double getAverageGrade() { - if (_studentBreakdown.isEmpty) return 0.0; - double sum = 0.0; - for (var student in _studentBreakdown) { - String? gradeStr = student['avgGrade']; - if (gradeStr == null || gradeStr.isEmpty) continue; - gradeStr = gradeStr.replaceAll('%', ''); - double? numericGrade = double.tryParse(gradeStr); - sum += numericGrade ?? 0.0; - } - return sum / _studentBreakdown.length; - } - - /// Returns the total number of quizzes submitted for the current quiz. - int getTotalSubmittedQuizzes() { - if (_questionBreakdown.isEmpty) return 0; - double grandTotalAttempts = 0; - int questionCount = _questionBreakdown.length; - for (QuestionStatsType q in _questionBreakdown) { - grandTotalAttempts += (q.numCorrect + q.numIncorrect + q.numPartial); - } - if (questionCount == 0) return 0; - return (grandTotalAttempts / questionCount).round(); - } - - Future _analyzeReport() async { - if (_studentBreakdown.isEmpty) return; - - // Check for available AI credentials - LlmType? selectedLLM; - if (LocalStorageService.userHasLlmKey(LlmType.CHATGPT)) { - selectedLLM = LlmType.CHATGPT; - } else if (LocalStorageService.userHasLlmKey(LlmType.GROK)) { - selectedLLM = LlmType.GROK; - } else if (LocalStorageService.userHasLlmKey(LlmType.PERPLEXITY)) { - selectedLLM = LlmType.PERPLEXITY; - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - "No AI credentials found. Please log in to an AI platform.")), - ); - return; - } - - String courseName = _selectedCourse?.fullName ?? "Unknown Course"; - String assessmentName = _selectedAssessment?.name ?? "Unknown Assessment"; - - // Build a summary string from the student breakdown data. - String studentSummary = _studentBreakdown.map((student) { - return "Name: ${student['studentName']}, Avg Grade: ${student['avgGrade']}, Rank: ${student['classRank']}"; - }).join("\n"); - - String prompt = - "Analyze the following analytics data for course '$courseName' and assignment '$assessmentName'.\n" - "Student Breakdown Data:\n$studentSummary\n" - "Based on this data, provide a thorough analysis indicating which students are excelling, " - "which are struggling, and which may not have completed the assignment. " - "Return your analysis as a JSON array where each element is an object with keys 'Student', 'Status', and 'Comments'."; - - // Select the AI model based on the available credentials. - dynamic aiModel; - if (selectedLLM == LlmType.CHATGPT) { - aiModel = OpenAiLLM(LocalStorageService.getOpenAIKey()); - } else if (selectedLLM == LlmType.GROK) { - aiModel = GrokLLM(LocalStorageService.getGrokKey()); - } else { - aiModel = PerplexityLLM(LocalStorageService.getPerplexityKey()); - } - - setState(() { - _isAnalyzingAI = true; - }); - - try { - var result = await aiModel.postToLlm(prompt); - String normalizedResult = result.trim(); - // Remove markdown code block wrappers if present. - if (normalizedResult.startsWith("```json")) { - normalizedResult = normalizedResult.substring(7); - } - if (normalizedResult.endsWith("```")) { - normalizedResult = - normalizedResult.substring(0, normalizedResult.length - 3); - } - normalizedResult = normalizedResult.trim(); - - var jsonData = json.decode(normalizedResult); - if (jsonData is List) { - setState(() { - _aiAnalysisData = List>.from(jsonData); - }); - } else { - setState(() { - _aiAnalysisData = []; - }); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("AI analysis did not return a valid JSON array.")), - ); - } - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Error during AI analysis: $e")), - ); - } finally { - setState(() { - _isAnalyzingAI = false; - }); - } - } -} diff --git a/team_a/teamA/lib/Views/assessments_view.dart b/team_a/teamA/lib/Views/assessments_view.dart deleted file mode 100644 index 208eb094..00000000 --- a/team_a/teamA/lib/Views/assessments_view.dart +++ /dev/null @@ -1,405 +0,0 @@ -import "package:flutter/material.dart"; -import 'package:learninglens_app/Api/lms/enum/lms_enum.dart'; -import "package:learninglens_app/Api/lms/factory/lms_factory.dart"; -import 'package:learninglens_app/Api/lms/google_classroom/google_lms_service.dart'; -import 'package:learninglens_app/beans/g_question_form_data.dart'; -import 'package:learninglens_app/beans/quiz.dart'; -import 'package:learninglens_app/beans/course.dart'; -import "package:learninglens_app/Controller/custom_appbar.dart"; -import "package:learninglens_app/beans/quiz_type.dart"; -import 'package:learninglens_app/content_carousel.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; -import 'package:flutter/services.dart'; -import 'package:url_launcher/url_launcher.dart'; -import "package:learninglens_app/Views/view_quiz.dart"; - -class AssessmentsView extends StatefulWidget { - AssessmentsView({super.key, this.quizID = 0, this.courseID = 0}); - - final int quizID; - final int? courseID; - - @override - _AssessmentsState createState() => _AssessmentsState(); -} - -class _AssessmentsState extends State { - late Future?> quizzes; - Quiz? selectedQuiz; - List> questionsData = []; - Future? _formDataFuture; - GoogleLmsService googleLmsService = GoogleLmsService(); - - @override - void initState() { - super.initState(); - _refreshQuizzes(); - } - - // Check if Moodle is selected - bool isMoodle() { - return LocalStorageService.getSelectedClassroom() == LmsType.MOODLE; - } - - // Method to refresh quizzes - void _refreshQuizzes() { - setState(() { - quizzes = getAllQuizzes(); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: 'Assessments', - onRefresh: _refreshQuizzes, - userprofileurl: LmsFactory.getLmsService().profileImage ?? '', - ), - body: Column( - children: [ - Row(children: [ - Padding( - padding: const EdgeInsets.all(4.0), - child: CreateButton('assessment')) - ]), - Expanded( - child: FutureBuilder?>( - future: quizzes, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center(child: Text('Error loading quizzes')); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return Center(child: Text('No quizzes found')); - } else { - final quizList = snapshot.data!; - - return LayoutBuilder( - builder: (context, constraints) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - flex: 1, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8.0), - ), - margin: EdgeInsets.all(8.0), - child: ListView.builder( - itemCount: quizList.length, - itemBuilder: (context, index) { - final quiz = quizList[index]; - final activeCourse = - getCourse(quiz.coursedId); - if (quiz.id == widget.quizID) { - selectedQuiz = quiz; - } - - return ListTile( - title: Text( - '${quiz.name} (${activeCourse.shortName}${activeCourse.courseId})'), - subtitle: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'Due: ${quiz.timeClose == null ? "No due date set" : Course.dateFormatted(quiz.timeClose!)}'), - ], - ), - tileColor: selectedQuiz == quiz - ? Theme.of(context) - .colorScheme - .primary - .withOpacity(0.1) - : null, - onTap: () { - setState(() { - selectedQuiz = quiz; - if (!isMoodle()) { - _formDataFuture = googleLmsService - .getAssignmentFormQuestions( - quiz.coursedId.toString(), - quiz.id.toString()); - } - }); - }, - ); - }, - ), - ), - ), - Expanded( - flex: 2, - child: selectedQuiz == null - ? Center( - child: - Text('Select a quiz to view details')) - : isMoodle() - ? _buildMoodleContent() - : _buildGoogleContent(), - ), - ], - ); - }, - ); - } - }, - ), - ), - ], - ), - ); - } - - Widget _buildMoodleContent() { - return Expanded( - flex: 2, - child: selectedQuiz == null && widget.quizID == 0 - ? Center(child: Text('Select a quiz to view details')) - : ViewQuiz(quizId: selectedQuiz?.id ?? widget.quizID), - ); - } - - Widget _buildGoogleContent() { - return FutureBuilder( - future: _formDataFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center(child: Text('Error: ${snapshot.error}')); - } else if (snapshot.hasData && snapshot.data!.questions.isNotEmpty) { - return DynamicForm(formData: snapshot.data!); - } else { - return Center(child: Text('No questions found in the Google Form.')); - } - }, - ); - } -} - -Future> getAllQuizzes() async { - print("Getting all quizzes"); - List result = []; - for (Course c in LmsFactory.getLmsService().courses ?? []) { - result.addAll(c.quizzes ?? []); - } - return result; -} - -Course getCourse(int? courseID) { - for (Course c in LmsFactory.getLmsService().courses ?? []) { - if (c.id == courseID) { - return c; - } - } - throw "No course found."; -} - -class DynamicForm extends StatelessWidget { - final FormData formData; - - const DynamicForm({super.key, required this.formData}); - - String _getQuestionType(List options) { - if (options.isEmpty) { - return 'Short Answer'; - } else if (options.length == 2) { - return 'True/False'; - } else { - return 'Multiple Choice'; - } - } - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Center( - child: Padding( - padding: const EdgeInsets.fromLTRB(16.0, 4.0, 16.0, 16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Text( - formData.title, - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - ), - Card( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Start Date: ', - style: TextStyle(fontWeight: FontWeight.bold)), - Text(formData.startDate ?? 'N/A'), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('End Date: ', - style: TextStyle(fontWeight: FontWeight.bold)), - Text(formData.endDate ?? 'N/A'), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Form URL: ', - style: TextStyle(fontWeight: FontWeight.bold)), - GestureDetector( - onTap: () async { - if (formData.formUrl != null) { - final Uri url = Uri.parse(formData.formUrl!); - if (await canLaunchUrl(url)) { - await launchUrl(url); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Cannot launch URL')), - ); - } - } - }, - child: Text( - formData.formUrl ?? 'N/A', - style: const TextStyle( - color: Colors.blue, - decoration: TextDecoration.underline, - ), - ), - ), - if (formData.formUrl != null) ...[ - IconButton( - icon: const Icon(Icons.copy), - onPressed: () { - Clipboard.setData( - ClipboardData(text: formData.formUrl!)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('URL copied to clipboard')), - ); - }, - ), - ], - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Status: ', - style: TextStyle(fontWeight: FontWeight.bold)), - Text(formData.status ?? 'N/A'), - ], - ), - const SizedBox(height: 16), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: DataTable( - columnSpacing: 20, - dataRowHeight: 60, - headingRowColor: MaterialStateProperty.all( - Colors.blueAccent.withOpacity(0.1)), - border: TableBorder.all( - color: Colors.grey, - width: 1.0, - ), - columns: const [ - DataColumn( - label: Center( - child: Text('Question No.', - style: - TextStyle(fontWeight: FontWeight.bold)), - ), - ), - DataColumn( - label: Center( - child: Text('Question', - style: - TextStyle(fontWeight: FontWeight.bold)), - ), - ), - DataColumn( - label: Center( - child: Text('Type', - style: - TextStyle(fontWeight: FontWeight.bold)), - ), - ), - DataColumn( - label: Center( - child: Text('Options', - style: - TextStyle(fontWeight: FontWeight.bold)), - ), - ), - ], - rows: formData.questions.asMap().entries.map((entry) { - int index = entry.key + 1; - QuestionData questionData = entry.value; - return DataRow(cells: [ - DataCell(Text('$index')), - DataCell( - SizedBox( - width: 400, - child: Text( - questionData.question, - softWrap: true, - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - ), - ), - DataCell( - Text( - _getQuestionType(questionData.options), - ), - ), - DataCell( - SizedBox( - width: 300, - child: Text( - questionData.options.isEmpty - ? 'N/A' - : questionData.options.join(', '), - softWrap: true, - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - ), - ), - ]); - }).toList(), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/team_a/teamA/lib/Views/chat_screen.dart b/team_a/teamA/lib/Views/chat_screen.dart deleted file mode 100644 index 6b2f63ff..00000000 --- a/team_a/teamA/lib/Views/chat_screen.dart +++ /dev/null @@ -1,248 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:learninglens_app/Api/llm/openai_api.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:shared_preferences/shared_preferences.dart'; // For saving/loading chat history -import 'package:learninglens_app/Controller/custom_appbar.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; -import 'package:learninglens_app/Api/llm/enum/llm_enum.dart'; -import 'package:learninglens_app/Api/llm/grok_api.dart'; -import 'package:learninglens_app/Api/llm/perplexity_api.dart'; -import 'dart:convert'; // For encoding and decoding chat history - -class ChatScreen extends StatefulWidget { - @override - _ChatScreenState createState() => _ChatScreenState(); -} - -class _ChatScreenState extends State { - final TextEditingController _controller = TextEditingController(); - List> _messages = []; - bool _isLoading = false; - String _role = 'student'; // Role toggle for student/teacher - final ScrollController _scrollController = - ScrollController(); // For scrolling the chat - SharedPreferences? _prefs; // SharedPreferences for saving chat history - LlmType? selectedLLM; - - @override - void initState() { - super.initState(); - _loadChatHistory(); // Load chat history when screen is initialized - selectedLLM = LlmType.CHATGPT; - } - - // Load chat history from SharedPreferences - Future _loadChatHistory() async { - _prefs = await SharedPreferences.getInstance(); - String? savedMessages = _prefs?.getString('chat_history'); - if (savedMessages != null) { - setState(() { - _messages = List>.from(jsonDecode(savedMessages)); - }); - } - } - - // Save chat history to SharedPreferences - Future _saveChatHistory() async { - if (_prefs != null) { - await _prefs?.setString('chat_history', jsonEncode(_messages)); - } - } - - // Function to handle user message sending and API response - Future _sendMessage() async { - final input = _controller.text; - - if (input.isEmpty) { - return; - } - final aiModel; - if (selectedLLM == LlmType.CHATGPT) { - aiModel = OpenAiLLM(LocalStorageService.getOpenAIKey()); - } else if (selectedLLM == LlmType.GROK) { - aiModel = GrokLLM(LocalStorageService.getGrokKey()); - } else if (selectedLLM == LlmType.PERPLEXITY) { - // aiModel = OpenAiLLM(perplexityApiKey); - aiModel = PerplexityLLM(LocalStorageService.getPerplexityKey()); - } else { - // default - aiModel = OpenAiLLM(LocalStorageService.getOpenAIKey()); - } - - // Update UI to show user's message and reset text field - setState(() { - _messages.add({'text': input, 'sender': 'user'}); - _isLoading = true; // Show a loading indicator while waiting for response - }); - - _controller.clear(); // Clear the input field - _scrollToBottom(); // Scroll to the bottom after sending the message - await _saveChatHistory(); // Save chat history - - try { - // Get ChatGPT response - // final chatGPTService = OpenAiLLM(); - final prompt = - _role == 'teacher' ? "You are assisting a teacher. $input" : input; - final response = await aiModel.getChatResponse(prompt); - - setState(() { - _messages.add({'text': response, 'sender': 'bot'}); - _isLoading = false; - }); - - _scrollToBottom(); // Scroll to the bottom after receiving the bot response - await _saveChatHistory(); // Save chat history after bot response - } catch (error) { - setState(() { - _messages.add({ - 'text': 'Error: Could not fetch response. Please try again.', - 'sender': 'bot' - }); - _isLoading = false; - }); - } - } - - // Function to clear chat history - void _clearChat() { - setState(() { - _messages.clear(); - }); - _saveChatHistory(); // Save the empty state to clear saved chat history - } - - // Function to toggle role (student/teacher) - // void _toggleRole() { ***** Not used ***** - // setState(() { - // _role = _role == 'student' ? 'teacher' : 'student'; - // }); - // } - - // Scroll to the bottom of the chat list - void _scrollToBottom() { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent, - duration: Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: 'Ask Chatbot!', - userprofileurl: LmsFactory.getLmsService().profileImage ?? '', - ), - backgroundColor: Theme.of(context).colorScheme.surface, - body: Row( - children: [ - // Main chat content - Expanded( - child: Column( - children: [ - Expanded( - child: ListView.builder( - controller: - _scrollController, // Attach the ScrollController - padding: const EdgeInsets.all(12), - itemCount: _messages.length, - itemBuilder: (context, index) { - final message = _messages[index]; - final isUserMessage = message['sender'] == 'user'; - - return Align( - alignment: isUserMessage - ? Alignment.centerRight - : Alignment.centerLeft, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 6), - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: isUserMessage - ? Colors.deepPurple - : Colors.grey[300], - borderRadius: BorderRadius.circular(16), - ), - child: Text( - message['text'], - style: TextStyle( - color: - isUserMessage ? Colors.white : Colors.black87, - fontSize: 16, - ), - ), - ), - ); - }, - ), - ), - if (_isLoading) - Padding( - padding: const EdgeInsets.all(8.0), - child: CircularProgressIndicator(), // Loading indicator - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _controller, - decoration: InputDecoration( - hintText: 'Enter your message...', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - filled: true, - fillColor: Colors.white, - contentPadding: EdgeInsets.symmetric( - vertical: 10, horizontal: 16), - ), - onSubmitted: (_) => _sendMessage(), - ), - ), - IconButton( - icon: Icon(Icons.send, color: Colors.purpleAccent), - onPressed: _sendMessage, - ), - IconButton( - icon: Icon(Icons.clear), - onPressed: _clearChat, // Clear chat history - ), - // IconButton( - // icon: Icon(Icons.switch_account), - // onPressed: _toggleRole, // Toggle role between teacher and student - // tooltip: - // 'Switch role to ${_role == 'student' ? 'Teacher' : 'Student'}', - // ), - DropdownButton( - value: selectedLLM, - onChanged: (LlmType? newValue) { - setState(() { - selectedLLM = newValue; - }); - }, - items: LlmType.values.map((LlmType llm) { - return DropdownMenuItem( - value: llm, - enabled: LocalStorageService.userHasLlmKey(llm), - child: Text(llm.displayName, style: TextStyle( - color: LocalStorageService.userHasLlmKey(llm) ? Colors.black87 : Colors.grey, - ), - ), - ); - }).toList() - ), - ], - ), - ), - ], - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/Views/course_content.dart b/team_a/teamA/lib/Views/course_content.dart deleted file mode 100644 index 2aadc20b..00000000 --- a/team_a/teamA/lib/Views/course_content.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:flutter/material.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'; -import '../content_carousel.dart'; - -//What we need: -//Two carousels, one for essays and the other for assessments. -//Additional information and buttons appear when a card is clicked. -//The essay cards have two buttons leading to submissions and assignment editing pages. -//The assessment cards have a button leading to the assessment editing page. -//Two buttons below that lead to the create essay and create assessment pages. - -//Main Page -class ViewCourseContents extends StatefulWidget { - final Course theCourse; - ViewCourseContents(this.theCourse); - - @override - State createState() { - return _CourseState(); - } -} - -class _CourseState extends State { - late final String courseName; - - @override - void initState() { - super.initState(); - courseName = widget.theCourse.fullName; - } - -@override -Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar(title: 'Course Content', userprofileurl: LmsFactory.getLmsService().profileImage ?? ''), - body: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, // Keeps everything left-aligned - children: [ - // Background for "Quizzes" - Container( - width: double.infinity, - color: Theme.of(context).colorScheme.secondary, - padding: const EdgeInsets.all(8.0), // Padding inside the container - margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), // Space around the container - child: Text( - "Quizzes", - style: TextStyle( - fontSize: 32, - color: Theme.of(context).colorScheme.onSecondary, // Text color set for contrast - ), - ), - ), - ContentCarousel( - 'assessment', - widget.theCourse.quizzes, - courseId: widget.theCourse.id, - ), - - // Background for "Essays" - Container( - width: double.infinity, - color: Theme.of(context).colorScheme.secondary, - padding: const EdgeInsets.all(8.0), - margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Text( - "Essays", - style: TextStyle( - fontSize: 32, - color: Theme.of(context).colorScheme.onSecondary, - ), - ), - ), - ContentCarousel( - 'essay', - widget.theCourse.essays, - courseId: widget.theCourse.id, - ), - - // Responsive layout for the buttons - LayoutBuilder( - builder: (context, constraints) { - if (constraints.maxWidth < 600) { - // Stack buttons vertically for narrow screens - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - CreateButton('assessment'), - SizedBox(height: 8.0), // Space between buttons - CreateButton('essay'), - ], - ); - } else { - // Show buttons in a row for wide screens - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - CreateButton('assessment'), - CreateButton('essay'), - ], - ); - } - }, - ), - ], - ), - ), - ); -} - - -} diff --git a/team_a/teamA/lib/Views/course_list.dart b/team_a/teamA/lib/Views/course_list.dart deleted file mode 100644 index 23572a46..00000000 --- a/team_a/teamA/lib/Views/course_list.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:learninglens_app/Api/lms/lms_interface.dart'; -import 'package:learninglens_app/Controller/custom_appbar.dart'; -import 'package:learninglens_app/beans/course.dart'; -import '../content_carousel.dart'; - -class CourseList extends StatefulWidget { - CourseList({super.key}); - - @override - _CourseListState createState() => _CourseListState(); -} - -class _CourseListState extends State { - final LmsInterface api = LmsFactory.getLmsService(); - late Future> courses; - Course? selectedCourse; - - @override - void initState() { - super.initState(); - courses = api.getUserCourses(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: 'Courses', - userprofileurl: LmsFactory.getLmsService().profileImage ?? '', - ), - body: FutureBuilder>( - future: courses, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center(child: Text('Error loading courses')); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return Center(child: Text('No courses found')); - } else { - final courseList = snapshot.data!; - - return LayoutBuilder( - builder: (context, constraints) { - return Row( - children: [ - // Left-side course list with border - Expanded( - flex: 1, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8.0), - ), - margin: EdgeInsets.all(8.0), - child: ListView.builder( - itemCount: courseList.length, - itemBuilder: (context, index) { - final course = courseList[index]; - return ListTile( - title: Text(course.fullName), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('${course.shortName}${course.courseId}'), - Text('${Course.dateFormatted(course.startdate)} - ${Course.dateFormatted(course.enddate)}'), - ], - ), - tileColor: selectedCourse == course - ? Theme.of(context).colorScheme.primary.withOpacity(0.1) - : null, - onTap: () { - setState(() { - selectedCourse = course; - }); - }, - ); - }, - ), - ), - ), - - // Right-side course details (essays and quizzes) - Expanded( - flex: 2, - child: selectedCourse == null - ? Center(child: Text('Select a course to view details')) - : Column( - key: ValueKey(selectedCourse!.id), // Force rebuild on course change - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Essays section - Container( - width: double.infinity, - color: Theme.of(context).colorScheme.secondary, - padding: const EdgeInsets.all(8.0), - margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Text( - "Essays", - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSecondary, - ), - ), - ), - Expanded( - child: ContentCarousel( - 'essay', - selectedCourse!.essays, - courseId: selectedCourse!.id, - ), - ), - - // Quizzes section - Container( - width: double.infinity, - color: Theme.of(context).colorScheme.secondary, - padding: const EdgeInsets.all(8.0), - margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - child: Text( - "Quizzes", - style: TextStyle( - fontSize: 24, - color: Theme.of(context).colorScheme.onSecondary, - ), - ), - ), - Expanded( - child: ContentCarousel( - 'assessment', - selectedCourse!.quizzes, - courseId: selectedCourse!.id, - ), - ), - ], - ), - ), - ], - ); - }, - ); - } - }, - ), - ); - } -} diff --git a/team_a/teamA/lib/Views/dashboard.dart b/team_a/teamA/lib/Views/dashboard.dart deleted file mode 100644 index d80cc950..00000000 --- a/team_a/teamA/lib/Views/dashboard.dart +++ /dev/null @@ -1,480 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:learninglens_app/Api/lms/enum/lms_enum.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:learninglens_app/Controller/custom_appbar.dart'; -import 'package:learninglens_app/Views/analytics_page.dart'; -import 'package:learninglens_app/Views/assessments_view.dart'; -import 'package:learninglens_app/Views/course_list.dart'; -import 'package:learninglens_app/Views/essays_view.dart'; -import 'package:learninglens_app/Views/about_page.dart'; -import 'package:learninglens_app/Views/g_lesson_plan.dart'; -import 'package:learninglens_app/Views/iep_page.dart'; -import 'package:learninglens_app/Views/lesson_plans.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; - -class TeacherDashboard extends StatelessWidget { - const TeacherDashboard({super.key}); - - @override - Widget build(BuildContext context) { - final bool canAccessApp = canUserAccessApp(context); - - return Scaffold( - appBar: CustomAppBar( - title: 'Learning Lens', - userprofileurl: LmsFactory.getLmsService().profileImage ?? '', - ), - backgroundColor: Theme.of(context).colorScheme.surface, - body: Column( - children: [ - if (!canAccessApp) - Container( - color: Colors.red[700], - padding: const EdgeInsets.all(12), - margin: const EdgeInsets.only(bottom: 10), - child: Row( - children: [ - const Icon(Icons.warning, color: Colors.white), - const SizedBox(width: 10), - Expanded( - child: Text( - "This application requires an LMS to be logged in and an LLM Key to function properly.", - style: const TextStyle(color: Colors.white), - ), - ), - ], - ), - ), - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - if (constraints.maxWidth > 600) { - return _buildDesktopLayout(context, constraints); - } else { - return _buildMobileLayout(context, constraints); - } - }, - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Center( - child: TextButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => AboutPage()), - ); - }, - child: const Text("About Learning Lens"), - ), - ), - ), - ], - ), - ); - } - - bool canUserAccessApp(BuildContext context) { - bool isLoggedIntoGoogleClassroom = LocalStorageService.isLoggedIntoGoogle() && LocalStorageService.hasLLMKey(); - bool isLoggedIntoMoodle = LocalStorageService.isLoggedIntoMoodle() && LocalStorageService.hasLLMKey(); - return isMoodle() ? isLoggedIntoMoodle : isLoggedIntoGoogleClassroom; - } - - String getClassroom() { - return LocalStorageService.getSelectedClassroom() == LmsType.MOODLE ? 'Moodle' : 'Google'; - } - - bool isMoodle() { - print(LocalStorageService.getSelectedClassroom()); - return LocalStorageService.getSelectedClassroom() == LmsType.MOODLE; - } - - Widget _buildDesktopLayout(BuildContext context, BoxConstraints constraints) { - final double screenWidth = constraints.maxWidth; - - double baseButtonSize = screenWidth * 0.15; - double baseButtonFontSize = screenWidth * 0.015; - double baseDescriptionFontSize = screenWidth * 0.015; - - double middleButtonSize = baseButtonSize * 1.2; - double middleButtonFontSize = baseButtonFontSize * 1.2; - double middleDescriptionFontSize = baseDescriptionFontSize * 1.1; - - baseButtonSize = baseButtonSize.clamp(80.0, 150.0); - baseButtonFontSize = baseButtonFontSize.clamp(12.0, 18.0); - baseDescriptionFontSize = baseDescriptionFontSize.clamp(12.0, 18.0); - - middleButtonSize = middleButtonSize.clamp(96.0, 180.0); - middleButtonFontSize = middleButtonFontSize.clamp(14.0, 20.0); - middleDescriptionFontSize = middleDescriptionFontSize.clamp(13.0, 20.0); - - double titleFontSize = screenWidth * 0.03; - titleFontSize = titleFontSize.clamp(20.0, 32.0); - - return SingleChildScrollView( - child: Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Teacher ${getClassroom()} Dashboard', - style: TextStyle( - fontSize: titleFontSize, - fontWeight: FontWeight.normal, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - const SizedBox(height: 12), - Text( - 'Welcome, ${LmsFactory.getLmsService().firstName ?? 'User'}', - style: TextStyle( - fontSize: titleFontSize * 0.7, - fontWeight: FontWeight.normal, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - const SizedBox(height: 20), - _buildGridLayout(context, constraints), - ], - ), - ), - ), - ); - } - - Widget _buildMobileLayout(BuildContext context, BoxConstraints constraints) { - final double screenWidth = constraints.maxWidth; - - double baseButtonSize = screenWidth * 0.35; // Reduced from 0.4 - double baseButtonFontSize = screenWidth * 0.04; // Reduced from 0.045 - double baseDescriptionFontSize = screenWidth * 0.035; // Reduced from 0.04 - - double middleButtonSize = baseButtonSize * 1.1; - double middleButtonFontSize = baseButtonFontSize * 1.1; - double middleDescriptionFontSize = baseDescriptionFontSize * 1.05; - - baseButtonSize = baseButtonSize.clamp(70.0, 120.0); // Reduced max size - baseButtonFontSize = baseButtonFontSize.clamp(10.0, 14.0); // Reduced max size - baseDescriptionFontSize = baseDescriptionFontSize.clamp(10.0, 14.0); // Reduced max size - - middleButtonSize = middleButtonSize.clamp(77.0, 132.0); - middleButtonFontSize = middleButtonFontSize.clamp(11.0, 16.0); - middleDescriptionFontSize = middleDescriptionFontSize.clamp(11.0, 15.0); - - double titleFontSize = screenWidth * 0.06; - titleFontSize = titleFontSize.clamp(16.0, 22.0); // Reduced max size - - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(12.0), // Reduced from 16.0 - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 600), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Teacher Dashboard', - style: TextStyle( - fontSize: titleFontSize, - fontWeight: FontWeight.normal, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - const SizedBox(height: 8), // Reduced from 12 - Text( - 'Welcome, ${LmsFactory.getLmsService().firstName ?? 'User'}', - style: TextStyle( - fontSize: titleFontSize * 0.7, - fontWeight: FontWeight.normal, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - const SizedBox(height: 12), // Reduced from 20 - _buildGridLayout(context, constraints), - ], - ), - ), - ), - ); - } - - Widget _buildGridLayout(BuildContext context, BoxConstraints constraints) { - final double screenWidth = constraints.maxWidth; - - double baseButtonSize = screenWidth * 0.15; - double baseButtonFontSize = screenWidth * 0.015; - double baseDescriptionFontSize = screenWidth * 0.015; - - baseButtonSize = baseButtonSize.clamp(70.0, 130.0); // Reduced max size - baseButtonFontSize = baseButtonFontSize.clamp(10.0, 16.0); // Reduced max size - baseDescriptionFontSize = baseDescriptionFontSize.clamp(10.0, 16.0); // Reduced max size - - bool canAccessApp = canUserAccessApp(context); - bool isMoodleSelected = isMoodle(); - - List> buttonData = [ - { - 'title': 'Courses', - 'description': 'View available courses.', - 'onPressed': !canAccessApp - ? null - : () => Navigator.push( - context, MaterialPageRoute(builder: (context) => CourseList())), - 'color': Colors.blue, - }, - { - 'title': 'Essays', - 'description': 'View or grade essays.', - 'onPressed': !canAccessApp - ? null - : () => Navigator.push( - context, MaterialPageRoute(builder: (context) => EssaysView())), - 'color': Colors.red, - }, - { - 'title': 'IEP', - 'description': 'Manage Individualized Education Plans.', - 'onPressed': !canAccessApp || !isMoodleSelected - ? null - : () => Navigator.push( - context, MaterialPageRoute(builder: (context) => IepPage())), - 'color': !isMoodleSelected ? Colors.grey : Colors.green, - }, - { - 'title': 'Analytics', - 'description': 'View performance analytics.', - 'onPressed': !canAccessApp || !isMoodleSelected - ? null - : () => Navigator.push( - context, MaterialPageRoute(builder: (context) => AnalyticsPage())), - 'color': !isMoodleSelected ? Colors.grey : Colors.cyan, - }, - { - 'title': 'Lesson Plan', - 'description': 'Create and manage lesson plans.', - 'onPressed': !canAccessApp - ? null - : () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => !isMoodleSelected - ? GoogleLessonPlans() - : LessonPlans(), - ), - ), - 'color': Colors.purple, - }, - { - 'title': 'Assessments', - 'description': 'Create or view assessments.', - 'onPressed': !canAccessApp - ? null - : () => Navigator.push( - context, MaterialPageRoute(builder: (context) => AssessmentsView())), - 'color': Colors.orange, - }, - ]; - - return Padding( - padding: const EdgeInsets.all(12.0), // Reduced from 16.0 - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: _buildResponsiveColumn( - context, - buttonData[0]['description'], - buttonData[0]['title'], - baseDescriptionFontSize, - baseButtonSize, - baseButtonFontSize, - buttonData[0]['onPressed'], - buttonData[0]['color'], - ), - ), - const SizedBox(width: 12), // Reduced from 20 - Expanded( - child: _buildResponsiveColumn( - context, - buttonData[1]['description'], - buttonData[1]['title'], - baseDescriptionFontSize, - baseButtonSize, - baseButtonFontSize, - buttonData[1]['onPressed'], - buttonData[1]['color'], - ), - ), - ], - ), - const SizedBox(height: 12), // Reduced from 20 - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: _buildResponsiveColumn( - context, - buttonData[2]['description'], - buttonData[2]['title'], - baseDescriptionFontSize, - baseButtonSize, - baseButtonFontSize, - buttonData[2]['onPressed'], - buttonData[2]['color'], - ), - ), - const SizedBox(width: 12), // Reduced from 20 - Expanded( - child: _buildResponsiveColumn( - context, - buttonData[3]['description'], - buttonData[3]['title'], - baseDescriptionFontSize, - baseButtonSize, - baseButtonFontSize, - buttonData[3]['onPressed'], - buttonData[3]['color'], - ), - ), - ], - ), - const SizedBox(height: 12), // Reduced from 20 - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: _buildResponsiveColumn( - context, - buttonData[4]['description'], - buttonData[4]['title'], - baseDescriptionFontSize, - baseButtonSize, - baseButtonFontSize, - buttonData[4]['onPressed'], - buttonData[4]['color'], - ), - ), - const SizedBox(width: 12), // Reduced from 20 - Expanded( - child: _buildResponsiveColumn( - context, - buttonData[5]['description'], - buttonData[5]['title'], - baseDescriptionFontSize, - baseButtonSize, - baseButtonFontSize, - buttonData[5]['onPressed'], - buttonData[5]['color'], - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildResponsiveColumn( - BuildContext context, - String description, - String title, - double descriptionFontSize, - double buttonSize, - double buttonFontSize, - void Function()? onPressed, - Color buttonColor, - ) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: buttonSize * 1.5, - child: Text( - description, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: descriptionFontSize, - color: Colors.black, - ), - ), - ), - const SizedBox(height: 8), // Reduced from 10 - _buildDashboardButton( - context, - title, - buttonSize, - buttonFontSize, - onPressed, - buttonColor, - ), - ], - ); - } - - Widget _buildDashboardButton( - BuildContext context, - String title, - double size, - double fontSize, - void Function()? onPressed, - Color buttonColor, - ) { - return Container( - height: size, - width: size, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: buttonColor, - boxShadow: [ - BoxShadow( - color: Colors.grey[500]!, - offset: const Offset(4, 4), - blurRadius: 15, - spreadRadius: 1, - ), - ], - ), - child: Container( - margin: EdgeInsets.all(size * 0.1), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.grey[600]!, - offset: const Offset(4, 4), - blurRadius: 10, - spreadRadius: 1, - ), - ], - ), - child: ElevatedButton( - onPressed: onPressed, - style: ElevatedButton.styleFrom( - shape: const CircleBorder(), - backgroundColor: Colors.transparent, - padding: EdgeInsets.all(size * 0.15), - shadowColor: Colors.transparent, - ), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - title, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: fontSize, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/Views/edit_questions.dart b/team_a/teamA/lib/Views/edit_questions.dart deleted file mode 100644 index dd2df280..00000000 --- a/team_a/teamA/lib/Views/edit_questions.dart +++ /dev/null @@ -1,212 +0,0 @@ -import 'package:learninglens_app/Api/llm/enum/llm_enum.dart'; -import 'package:learninglens_app/Api/llm/grok_api.dart'; -import 'package:learninglens_app/Api/llm/perplexity_api.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:learninglens_app/Controller/custom_appbar.dart'; -import 'package:learninglens_app/Views/send_quiz_to_moodle.dart'; -import 'package:learninglens_app/Api/llm/openai_api.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; -import 'package:learninglens_app/beans/quiz.dart'; -import 'package:learninglens_app/beans/question.dart'; -import 'package:flutter/material.dart'; -import 'quiz_generator.dart'; - -class EditQuestions extends StatefulWidget { - final String questionXML; - - EditQuestions(this.questionXML); - - @override - EditQuestionsState createState() => EditQuestionsState(); -} - -class EditQuestionsState extends State { - late Quiz myQuiz; - // final TextEditingController _textController = TextEditingController(); - var apikey = LocalStorageService.getOpenAIKey(); - // late OpenAiLLM openai; - var aiModel; - bool _isLoading = false; - - String subject = CreateAssessment.descriptionController.text; - String topic = CreateAssessment.topicController.text; - late String promptstart; - - @override - void initState() { - super.initState(); - myQuiz = Quiz.fromXmlString(widget.questionXML); - getAiModel(); - // if (apikey.isNotEmpty) { - // // TODO: Fix only excepts ChatGPT AI. - // openai = OpenAiLLM(apikey); - // } else { - // // Handle the case where the API key is null - // throw Exception('API key is not set in the environment variables'); - // } - myQuiz.name = CreateAssessment.nameController.text; - myQuiz.description = CreateAssessment.descriptionController.text; - - promptstart = - 'Create a question that is compatible with Moodle XML import. ' - 'Be a bit creative in how you design the question and answers, ' - 'making sure it is engaging but still on the subject of $subject and related to $topic. ' - 'Make sure the XML specification is included, and the question is wrapped ' - 'in the quiz XML element required by Moodle. Each answer should have feedback ' - 'that fits the Moodle XML format, and avoid using HTML elements within a CDATA field. ' - 'The quiz should be challenging and thought-provoking, but appropriate for ' - 'high school students who speak English. The Moodle question type should be '; - } - - void getAiModel() { - LlmType selectedLLM = LlmType.values.firstWhere( - (type) => type.displayName == CreateAssessment.llmController.text, - orElse: () => throw Exception('No LlmType found'), - ); - if (selectedLLM == LlmType.CHATGPT) { - aiModel = OpenAiLLM(LocalStorageService.getOpenAIKey()); - } else if (selectedLLM == LlmType.GROK) { - aiModel = GrokLLM(LocalStorageService.getGrokKey()); - } else if (selectedLLM == LlmType.PERPLEXITY) { - // aiModel = OpenAiLLM(perplexityApiKey); - aiModel = PerplexityLLM(LocalStorageService.getPerplexityKey()); - } else { - // default - aiModel = OpenAiLLM(LocalStorageService.getOpenAIKey()); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: 'Edit Questions', - userprofileurl: LmsFactory.getLmsService().profileImage ?? '', - ), - body: Column( - children: [ - Text( - 'Swipe right to have the AI rebuild the question.', - style: TextStyle(fontSize: 14),), - Text( - 'Swipe left to remove the question', - style: TextStyle(fontSize: 14),), - Expanded( - child: ListView.builder( - itemCount: myQuiz.questionList.length, - itemBuilder: (context, index) { - var question = myQuiz.questionList[index]; - return Dismissible( - key: Key(question.toString()), - background: Stack( - children: [ - Container( - color: Theme.of(context).colorScheme.scrim, - child: Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(left: 16), - child: Icon( - Icons.refresh, - color: Theme.of(context).colorScheme.surface, - ), - ), - ), - ), - if (_isLoading) - Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.surface, - ), - ), // Spinner behind the item - ), - ], - ), - secondaryBackground: Container( - color: Theme.of(context).colorScheme.error, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.only(right: 16), - child: Icon(Icons.delete), - ), - ), - ), - confirmDismiss: (direction) async { - if (direction == DismissDirection.startToEnd) { - setState(() { - _isLoading = true; - }); - var result = await aiModel - // .postToLlm(promptstart + question.toString()); - .postToLlm(promptstart + question.type.toString()); - - setState(() { - _isLoading = false; // Stop showing the spinner - }); - - if (result.isNotEmpty) { - setState(() { - //replace the old question with the new one from the api call - question = Quiz.fromXmlString(result).questionList[0]; - question.setName = 'Question ${index + 1}'; - myQuiz.questionList[index] = question.copyWith( - isFavorite: !question.isFavorite); - }); - } - return false; - } else { - bool delete = true; - final snackbarController = - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Deleted $Question'), - duration: Duration(seconds: 2), - action: SnackBarAction( - label: 'Undo', onPressed: () => delete = false), - ), - ); - await snackbarController.closed; - return delete; - } - }, - onDismissed: (_) { - setState(() { - myQuiz.questionList.removeAt(index); - }); - }, - child: ListTile( - title: Text(question.toString()), - tileColor: (index.isEven) - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).colorScheme.secondaryContainer, - textColor: (index.isEven) - ? Theme.of(context).colorScheme.onSecondary - : Theme.of(context).colorScheme.onSecondaryContainer, - ), - ); - }, - ), - ), - Row( - children: [ - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => QuizMoodle(quiz: myQuiz) - ), - ); - }, - child: const Text('Accept questions and Submit'), - ), - - ], - ) - ], - ), - ); - } -} diff --git a/team_a/teamA/lib/Views/essay_edit_page.dart b/team_a/teamA/lib/Views/essay_edit_page.dart deleted file mode 100644 index 5551e5b4..00000000 --- a/team_a/teamA/lib/Views/essay_edit_page.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:editable/editable.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:learninglens_app/Controller/custom_appbar.dart'; -import 'dart:convert'; - -import 'send_essay_to_moodle.dart'; // Import for JSON encoding - -class EssayEditPage extends StatefulWidget { - final String jsonData; - final String description; - - EssayEditPage(this.jsonData, this.description); - - @override - EssayEditPageState createState() => EssayEditPageState(); // Public State class -} - -class EssayEditPageState extends State { - - - // Convert JSON to rows compatible with Editable - List rows = []; - - // Headers or Columns - List headers = []; - - @override - void initState() { - super.initState(); - - populateHeadersAndRows(); - } - - // Function to dynamically populate headers and rows based on JSON data - void populateHeadersAndRows() { - Map mappedData = jsonDecode(widget.jsonData); - // Step 1: Build headers dynamically based on the number of levels in the first criterion - List levels = List.from(mappedData['criteria']![0]['levels'] as List); -headers = [ - {"title": 'Criteria', 'index': 1, 'key': 'name', 'widthFactor': 0.15}, // 30% width -]; - -for (int i = 0; i < levels.length; i++) { - headers.add({ - "title": '${levels[i]['score']}', - 'index': i + 2, - 'key': 'level_$i', - 'widthFactor': 0.8/levels.length, // 10% width for each level column - }); -} - - // Step 2: Build rows by mapping each criterion and its levels dynamically - rows = (mappedData['criteria'] ?? []).map((criterion) { - Map row = { - "name": criterion['description'], - }; - - for (int i = 0; i < (criterion['levels'] as List).length; i++) { - row['level_$i'] = (criterion['levels'] as List)[i]['definition']; - } - - return row; - }).toList(); - - setState(() {}); // Ensure the UI is updated after populating headers and rows - } - - /// Create a Key for EditableState - final _editableKey = GlobalKey(); - - /// Merge edits into the original jsonData and return updated JSON - String getUpdatedJson() { - List editedRows = _editableKey.currentState!.editedRows; - Map mappedData = jsonDecode(widget.jsonData); - - // Apply the edits to the original jsonData - for (var editedRow in editedRows) { - int rowIndex = editedRow['row']; - var originalCriterion = mappedData['criteria']?[rowIndex]; - - // For each edited level, update the corresponding level in the original data - editedRow.forEach((key, value) { - if (key != 'row' && key.startsWith('level_')) { - int levelIndex = int.parse(key.split('_')[1]); - (originalCriterion as Map)['levels']?[levelIndex]['definition'] = value; - } - }); - } - - // Convert the updated jsonData back to the required format and return it - Map updatedData = { - "criteria": mappedData['criteria'] - }; - return jsonEncode(updatedData); // Return the JSON as a string - } - -@override -Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar(title: 'Edit Essay Rubric', userprofileurl: LmsFactory.getLmsService().profileImage ?? ''), - body: LayoutBuilder( - builder: (context, constraints) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, // Align content to the top-left - children: [ - SizedBox(height: 24.0), - - // Expanded is used for the Editable, wrapped with SingleChildScrollView for horizontal scrolling - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, // Allow horizontal scrolling for the Editable - child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: 600, // Ensure the table never shrinks below 600px - maxWidth: constraints.maxWidth > 600 ? constraints.maxWidth : 600, - ), - child: Editable( - key: _editableKey, - tdEditableMaxLines: 100, - trHeight: 100, - columns: headers, - rows: rows, - showCreateButton: false, - tdStyle: TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - showSaveIcon: false, - onRowSaved: (value) { - print('rowsaved $value'); - }, - borderColor: Theme.of(context).colorScheme.primaryContainer, - onSubmitted: (value) { - print('onsubmitted: $value'); // You can grab this data to store anywhere - }, - ), - ), - ), - ), - - SizedBox(height: 20), // Add some spacing between the Editable and the button - - // Row for the Button outside the scrollable area - Row( - mainAxisAlignment: MainAxisAlignment.center, // Center the button horizontally - children: [ - ElevatedButton( - child: const Text('Send to Moodle'), - onPressed: () { - String updatedJson = getUpdatedJson(); - // Navigate to the Essay Assignment Settings page with the updated JSON - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => - EssayAssignmentSettings(updatedJson, widget.description))); - print(updatedJson); // You can now see the updated JSON in the console - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Data sent to Moodle'))); - }, - ), - ], - ), - SizedBox(height: 20), // Optional additional spacing - ], - ); - }, - ), - ); -} - - - - - -} \ No newline at end of file diff --git a/team_a/teamA/lib/Views/essay_generation.dart b/team_a/teamA/lib/Views/essay_generation.dart deleted file mode 100644 index 5391c39a..00000000 --- a/team_a/teamA/lib/Views/essay_generation.dart +++ /dev/null @@ -1,604 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:learninglens_app/Api/lms/constants/learning_lens.constants.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:learninglens_app/Controller/custom_appbar.dart'; -import 'package:learninglens_app/Views/essay_edit_page.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; -import 'dart:convert'; -import '../Api/llm/perplexity_api.dart'; -import 'package:learninglens_app/Api/llm/openai_api.dart'; -import 'package:learninglens_app/Api/llm/grok_api.dart'; -import 'package:learninglens_app/Api/llm/enum/llm_enum.dart'; - - -// Required Components: -// 2 Dropdowns: 1 for the Grade Level and 1 for the Point Scale -// 3 Text Boxes: Standard/Objective, Assignment Description, Additional Customization for Rubric (Optional) -// Audio icon for each textbox for readback? -// Paper clip for each textbox for attachments? -// 2 Buttons: 1 Generate Essay Button, 1 Send to Moodle Button -// 1 Frame to show the rubric that was generated? - -class EssayGeneration extends StatefulWidget { - const EssayGeneration({super.key, required this.title}); - final String title; - - @override - State createState() => _EssayGenerationState(); -} - -class _EssayGenerationState extends State -{ - //Holds values for user input fields - int _selectedPointScale = 3; // Default value - String _selectedGradeLevel = LearningLensConstants.gradeLevels.last; // Default value for GradeLevelDropdown - bool _isLoading = false; - // String? selectedLLM = 'Perplexity'; // default - LlmType? selectedLLM; - - // llm options - final List llmOptions = ['ChatGPT', 'CLAUDE', 'Perplexity']; - - // Variables to store the text inputs - final TextEditingController _standardObjectiveController = - TextEditingController(); - final TextEditingController _assignmentDescriptionController = - TextEditingController(); - final TextEditingController _additionalCustomizationController = - TextEditingController(); - - dynamic globalRubric; - dynamic rubricasjson; - - // api keys - // final perplexityApiKey = LocalStorageService.getPerplexityKey(); - // final openApiKey = LocalStorageService.getOpenAIKey(); - // final claudeApiKey = LocalStorageService.getClaudeKey(); - - // event handlers - void _handlePointScaleChanged(int? newValue) { - setState(() { - if (newValue != null) { - _selectedPointScale = newValue; - } - }); - } - - // Function to store the selected grade level value - void _handleGradeLevelChanged(String? newValue) { - setState(() { - if (newValue != null) { - _selectedGradeLevel = newValue; - } - }); - } - - // Handle LLM Selection - void _handleLLMChanged(LlmType? newValue) { - setState(() { - if (newValue != null) { - selectedLLM = newValue; - } - }); - } - - // Get api key for selected LLM - String getApiKey() { - switch (selectedLLM) { - case LlmType.CHATGPT: - return LocalStorageService.getOpenAIKey(); - case LlmType.GROK: - return LocalStorageService.getGrokKey(); - case LlmType.PERPLEXITY: - return LocalStorageService.getPerplexityKey(); - default: - return LocalStorageService.getOpenAIKey(); - } - } - - Future pingApi(String inputs) async { - try { - setState(() { - _isLoading = true; // Set loading state to true - }); - - String apiKey = - getApiKey(); // Get the correct API key based on the selected LLM - if (apiKey.isEmpty) { - throw Exception("API key is missing"); - } - - // Dynamically instantiate the appropriate LLM class based on the selectedLLM - dynamic llmInstance; - if (selectedLLM == LlmType.CHATGPT) { - llmInstance = OpenAiLLM(getApiKey()); - } else if (selectedLLM == LlmType.GROK) { - llmInstance = GrokLLM(getApiKey()); - } else if (selectedLLM == LlmType.PERPLEXITY) { - llmInstance = PerplexityLLM(getApiKey()); // Perplexity API class - } else { - throw Exception('Invalid LLM selected.'); - } - - 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 {. - Remove any ``` from your output. - - You must reply with a representation of the rubric in JSON format that exactly matches this format: - { - "criteria": [ - { - "description": #CriteriaName, - "levels": [ - { "definition": #CriteriaDef, "score": #ScoreValue }, - ] - } - ] - } - #CriteriaName must be replaced with the name of the criteria. - #CriteriaDef must be replaced with a detailed description of what meeting that criteria would look like for each scale value. - #ScoreValue must be replaced with a number representing the score. The score for the lowest scale value will be 0, and the scores will increase by 1 for each scale. - You should create as many "levels" objects as there are point scale values. - Make sure the JSON exactly matches the format above, or you will receive an error. - Do not include any additional information in your response. - Here is the assignment information: - $inputs - '''; - globalRubric = await llmInstance.postToLlm(queryPrompt); - globalRubric = globalRubric.replaceAll('```', '').trim(); - return jsonDecode(globalRubric); - } catch (e) { - print("Error in API request: $e"); - return null; - } finally { - setState(() { - _isLoading = false; // Reset loading state to false - }); - } - } - - String getSelectedResponses() { - return ''' - Selected Grade Level: $_selectedGradeLevel - Selected Point Scale: $_selectedPointScale - Standard / Objective: ${_standardObjectiveController.text} - Assignment Description: ${_assignmentDescriptionController.text} - Additional Customization: ${_additionalCustomizationController.text} - '''; - } - - @override - void dispose() { - // Dispose the controllers when the widget is removed from the widget tree - _standardObjectiveController.dispose(); - _assignmentDescriptionController.dispose(); - _additionalCustomizationController.dispose(); - super.dispose(); - } - _EssayGenerationState(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar(title: 'Create Essay Rubric', userprofileurl: LmsFactory.getLmsService().profileImage ?? ''), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - // Using Row to split screen into two sections - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Left Column - Expanded( - flex: - 2, // This controls the space for the left side, bigger ratio - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text("Rubric Generator", - style: TextStyle(fontSize: 24)), - const SizedBox(height: 16), - - // Grade Level Dropdown - GradeLevelDropdown( - selectedGradeLevel: _selectedGradeLevel, - onChanged: _handleGradeLevelChanged, - ), - - const SizedBox(height: 16), - - // Point Scale Dropdown - PointScaleDropdown( - selectedPointScale: _selectedPointScale, - onChanged: _handlePointScaleChanged, - ), - - const SizedBox(height: 16), - - // LLM Selection Dropdown - DropdownButton( - value: selectedLLM, - onChanged: _handleLLMChanged, - // items: ['Perplexity', 'OpenAI', 'Claude'] - // .map>((String value) { - // return DropdownMenuItem( - // value: value, - // child: Text(value), - // ); - // }).toList(), - items: LlmType.values.map((LlmType llm) { - return DropdownMenuItem( - value: llm, - enabled: LocalStorageService.userHasLlmKey(llm), - child: Text(llm.displayName, style: TextStyle( - color: LocalStorageService.userHasLlmKey(llm) ? Colors.black87 : Colors.grey, - ), - ), - ); - }).toList() - ), - - const SizedBox(height: 16), - - // Standard/objective - TextBox( - label: "Standard / Objective", - // icon: Icons.mic, - // secondaryIcon: Icons.attachment, - initialValue: '', - onChanged: (newValue) { - _standardObjectiveController.text = newValue!; - }, - maxLines: 2, - ), - const SizedBox(height: 16), - - // Assignment description - TextBox( - label: "Assignment Description", - // icon: Icons.mic, - // secondaryIcon: Icons.attachment, - initialValue: '', - onChanged: (newValue) { - _assignmentDescriptionController.text = newValue!; - }, - maxLines: 2, - ), - const SizedBox(height: 16), - - // Additional customization - TextBox( - label: "Additional Customization for Rubric (Optional)", - // icon: Icons.mic, - // secondaryIcon: Icons.attachment, - initialValue: '', - onChanged: (newValue) { - _additionalCustomizationController.text = newValue!; - }, - maxLines: 2, - ), - - const SizedBox(height: 16), - - // Generate Button - Button( - 'essay', - onPressed: () { - final result = getSelectedResponses(); - - pingApi(result).then((dynamic results) { - setState(() { - rubricasjson = globalRubric; - globalRubric = results; // Store the rubric - }); - }); - }, - ), - ], - ), - ), - - const SizedBox(width: 32), // Adds space between the two columns - - // Right Side - Expanded( - flex: 3, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 600, - color: Colors.grey[200], - child: - _isLoading // Make the container dependent on the isLoading var - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator(), // Loading spinner - SizedBox(height: 16), - Text( - "Generating Rubric...", // Display this text when we start loading - style: TextStyle( - fontSize: 18, color: Colors.black54), - ), - ], - ), - ) - : globalRubric != null && - globalRubric['criteria'] != null - ? SingleChildScrollView( - child: Table( - border: TableBorder - .all(), // Adds border to the table cells - columnWidths: const { - 0: FlexColumnWidth( - 1), // Description column - // Dynamically add scores per column - }, - children: [ - TableRow( - children: [ - Padding( - padding: - const EdgeInsets.all(8.0), - child: Text( - 'Criteria', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16), - ), - ), - - // Dynamically create score level headers - for (var level in globalRubric[ - 'criteria'][0][ - 'levels']) // Assuming all criteria have the same number of levels - Padding( - padding: - const EdgeInsets.all(8.0), - child: Text( - '${level['score']}', - style: TextStyle( - fontWeight: - FontWeight.bold, - fontSize: 16), - textAlign: TextAlign.center, - ), - ), - ], - ), - - // Create rows - for (var criteria - in globalRubric['criteria']) ...[ - TableRow( - children: [ - Padding( - padding: - const EdgeInsets.all(8.0), - child: Text( - criteria['description'], - style: TextStyle( - fontWeight: - FontWeight.bold), - ), - ), - - // Add score levels for each column - for (var level - in criteria['levels']) - Padding( - padding: - const EdgeInsets.all(8.0), - child: Text( - '${level['definition']}', - textAlign: TextAlign.center, - ), - ), - ], - ), - ], - ], - ), - ) - : Center( - child: Text( - "No Rubric Data Available", - style: TextStyle( - fontSize: 18, color: Colors.black54), - ), - ), - ), - const SizedBox(height: 16), - // Send to Moodle Button - Button( - "assessment", - onPressed: rubricasjson != null - ? () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - EssayEditPage(rubricasjson, _assignmentDescriptionController.text), - ), - ); - } - : null, // Disable button when rubricasjson is null - ), - ], - ), - ), - ], - ), - ), - ); - } -} - -// Create Class for Buttons -class Button extends StatelessWidget { - // Fields in this widget subclass are marked final - final String type; - final String text; - final String filters = ""; - final VoidCallback? onPressed; - Button._(this.type, this.text, {this.onPressed}); - - factory Button(String type, {VoidCallback? onPressed}) { - if (type == "assessment") { - return Button._( - type, - "Continue (Edit Rubric)", - onPressed: onPressed, - ); - } else if (type == "essay") { - return Button._( - type, - "Generate Rubric", - onPressed: onPressed, - ); - } else { - return Button._(type, ""); - } - } - @override - Widget build(BuildContext context) { - return OutlinedButton( - onPressed: onPressed, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [Icon(Icons.create), Text(text)], - )); - } -} - -class TextBox extends StatefulWidget { - // Create stateful widget to maintain persistence in textboxes from events - final String label; - // final IconData icon; - // final IconData secondaryIcon; - final String initialValue; - final ValueChanged onChanged; - final int maxLines; - - const TextBox({ - super.key, - required this.label, - // required this.icon, - // required this.secondaryIcon, - required this.initialValue, - required this.onChanged, - this.maxLines = 1, - }); - - @override - TextBoxState createState() => TextBoxState(); -} - -class TextBoxState extends State { - // State within - late TextEditingController _controller; - - @override - void initState() { - super.initState(); - // Initialize controller with initial value - _controller = TextEditingController(text: widget.initialValue); - // Listen for changes - _controller.addListener(() { - widget.onChanged(_controller.text); // Call the passed onChanged function - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return TextField( - controller: _controller, // Use the controller initialized in initState - maxLines: widget.maxLines, - decoration: InputDecoration( - labelText: widget.label, - // prefixIcon: Column( - // mainAxisAlignment: MainAxisAlignment.center, - // children: [ - // Icon(widget.icon), - // SizedBox(height: 4), - // Icon(widget.secondaryIcon), - // ], - // ), - ), - ); - } -} - -class PointScaleDropdown extends StatelessWidget { - final int selectedPointScale; - // We need to store the value of the int the user inputs - final ValueChanged onChanged; - - const PointScaleDropdown({ - Key? key, - required this.selectedPointScale, - required this.onChanged, - }) : super(key: key); - - void _handleValueChanged(int? newValue) { - // Additional Logic - onChanged(newValue); // Call the passed onChanged function - } - - @override - Widget build(BuildContext context) { - return DropdownButtonFormField( - decoration: const InputDecoration(labelText: "Point Scale"), - value: selectedPointScale, - items: [1, 2, 3, 4, 5].map((int value) { - return DropdownMenuItem( - value: value, - child: Text(value.toString()), - ); - }).toList(), - onChanged: _handleValueChanged, - ); - } -} - -class GradeLevelDropdown extends StatelessWidget { - final String selectedGradeLevel; - // We need to store the value of the string the user inputs - final ValueChanged onChanged; - - const GradeLevelDropdown({ - Key? key, - required this.selectedGradeLevel, - required this.onChanged, - }) : super(key: key); - - void _handleTextChanged(String? newValue) { - // Additional Logic - onChanged(newValue); // Call the passed onChanged function - } - - @override - Widget build(BuildContext context) { - return DropdownButtonFormField( - decoration: const InputDecoration(labelText: "Grade Level"), - value: selectedGradeLevel.isNotEmpty ? selectedGradeLevel : null, - items: LearningLensConstants.gradeLevels - .map((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - onChanged: _handleTextChanged, - ); - } -} diff --git a/team_a/teamA/lib/Views/essays_view.dart b/team_a/teamA/lib/Views/essays_view.dart deleted file mode 100644 index 21f477d6..00000000 --- a/team_a/teamA/lib/Views/essays_view.dart +++ /dev/null @@ -1,333 +0,0 @@ -import "package:flutter/material.dart"; -import "package:learninglens_app/Api/lms/factory/lms_factory.dart"; -import "package:learninglens_app/Views/view_submissions.dart"; -import "package:learninglens_app/beans/assignment.dart"; -import "package:learninglens_app/beans/participant.dart"; -import "package:learninglens_app/beans/course.dart"; -import "package:learninglens_app/Controller/custom_appbar.dart"; -import "package:learninglens_app/beans/submission_with_grade.dart"; -import "package:learninglens_app/content_carousel.dart"; - -// The Page -class EssaysView extends StatefulWidget { - EssaysView({super.key, this.essayID = 0, this.courseID = 0}); - final int essayID; - final int? courseID; - - @override - _EssaysState createState() => _EssaysState(); -} - -class _EssaysState extends State { - late Future> essays; - Assignment? selectedEssay; - late Future> futureSubmissionsWithGrades; - late Future> futureParticipants; - String? submissionError; // To store error message for submissions - String? participantError; // To store error message for participants - - @override - void initState() { - super.initState(); - essays = getAllEssays(widget.courseID); - // Initialize with placeholder futures to avoid null errors - futureSubmissionsWithGrades = Future.value([]); - futureParticipants = Future.value([]); - } - - // Helper method to fetch submissions with error handling - Future> fetchSubmissionsWithGrades( - int essayId) async { - try { - return await LmsFactory.getLmsService().getSubmissionsWithGrades(essayId); - } on UnimplementedError catch (e) { - setState(() { - submissionError = - "Submissions/Grading feature is currently not available for Google Classroom. Please reach out to the developer for more information."; - }); - return []; // Return empty list to avoid breaking the FutureBuilder - } catch (e) { - setState(() { - submissionError = "Error fetching submissions: $e"; - }); - return []; - } - } - - // Helper method to fetch participants with error handling - Future> fetchCourseParticipants(String courseId) async { - try { - return await LmsFactory.getLmsService().getCourseParticipants(courseId); - } on UnimplementedError catch (e) { - setState(() { - participantError = "Participants feature is not yet implemented."; - }); - return []; // Return empty list to avoid breaking the FutureBuilder - } catch (e) { - setState(() { - participantError = "Error fetching participants: $e"; - }); - return []; - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: 'Essays', - onRefresh: () { - setState(() { - essays = getAllEssays(widget.courseID); - }); - }, - userprofileurl: LmsFactory.getLmsService().profileImage ?? '', - ), - body: Column( - children: [ - Row(children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: CreateButton('essay')) - ]), - Expanded( - child: FutureBuilder>( - future: essays, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center(child: Text('Error loading essays')); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return Center(child: Text('No essays found')); - } else { - final essayList = snapshot.data!; - - return LayoutBuilder( - builder: (context, constraints) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - flex: 1, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8.0), - ), - margin: EdgeInsets.all(8.0), - child: ListView.builder( - itemCount: essayList.length, - itemBuilder: (context, index) { - final essay = essayList[index]; - final activeCourse = - getCourse(essay.courseId); - if (essay.id == widget.essayID) { - selectedEssay = essay; - } - return ListTile( - title: Text( - '${essay.name} (${activeCourse.shortName}${activeCourse.courseId})'), - subtitle: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'Due: ${essay.dueDate == null ? "No due date set" : Course.dateFormatted(essay.dueDate!)}'), - ], - ), - tileColor: selectedEssay == essay - ? Theme.of(context) - .colorScheme - .primary - .withOpacity(0.1) - : null, - onTap: () { - setState(() { - selectedEssay = essay; - submissionError = null; // Reset error - participantError = null; // Reset error - futureSubmissionsWithGrades = - fetchSubmissionsWithGrades( - essay.id!); - futureParticipants = - fetchCourseParticipants( - essay.courseId.toString()); - }); - }, - ); - }, - ), - ), - ), - Expanded( - flex: 2, - child: selectedEssay == null && widget.essayID == 0 - ? Center( - child: - Text('Select an essay to view details')) - : Column( - children: [ - Container( - decoration: BoxDecoration( - border: - Border.all(color: Colors.grey), - borderRadius: - BorderRadius.circular(8.0), - ), - margin: EdgeInsets.all(8.0), - child: Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: - const EdgeInsets.all( - 8.0), - child: Text('Essay Prompt', - style: TextStyle( - fontSize: 20))), - Text(selectedEssay - ?.description ?? - getEssay(widget.essayID, - widget.courseID) - ?.description ?? - "No description is available."), - ], - ), - ), - ), - ), - Expanded( - flex: 1, - child: Container( - decoration: BoxDecoration( - border: - Border.all(color: Colors.grey), - borderRadius: - BorderRadius.circular(8.0), - ), - margin: EdgeInsets.all(8.0), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: - const EdgeInsets.all(8.0), - child: Text( - 'Student Submissions', - style: TextStyle( - fontSize: 20)), - ), - Expanded( - child: submissionError != null - ? Center( - child: Column( - mainAxisAlignment: - MainAxisAlignment - .center, - children: [ - Icon( - Icons - .construction, // Under development icon - size: 48.0, - color: Colors - .orange, - ), - SizedBox( - height: 16.0), - Text( - submissionError!, - textAlign: - TextAlign - .center, - style: - TextStyle( - fontSize: - 16.0, - color: Colors - .grey[ - 700], - ), - ), - ], - ), - ) - : SubmissionList( - key: ValueKey( - selectedEssay - ?.id ?? - widget - .essayID), - assignmentId: - selectedEssay - ?.id ?? - widget - .essayID, - courseId: selectedEssay - ?.courseId - .toString() ?? - widget.courseID - .toString(), - ), - ), - ], - ), - ), - ), - ), - ], - ), - ), - ], - ); - }, - ); - } - }, - ), - ), - ], - ), - ); - } -} - -// Helper function that pulls the quizzes from all the user's courses -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; -} - -Assignment? getEssay(int? essayID, int? courseID) { - Assignment? result; - for (Course c in LmsFactory.getLmsService().courses ?? []) { - if (courseID == 0 || courseID == null || c.id == courseID) { - for (Assignment a in c.essays ?? []) { - if (a.id == essayID) { - result = a; - } - } - } - } - return result; -} - -// Helper function that gets the course number for the quiz -Course getCourse(int? courseID) { - for (Course c in LmsFactory.getLmsService().courses ?? []) { - if (c.id == courseID) { - return c; - } - } - throw "No course found."; -} diff --git a/team_a/teamA/lib/Views/g_assignment_create.dart b/team_a/teamA/lib/Views/g_assignment_create.dart deleted file mode 100644 index 19d9a414..00000000 --- a/team_a/teamA/lib/Views/g_assignment_create.dart +++ /dev/null @@ -1,298 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:learninglens_app/Api/lms/google_classroom/google_classroom_api.dart'; -import 'package:learninglens_app/Controller/custom_appbar.dart'; -import 'package:learninglens_app/Controller/main_controller.dart'; -import 'package:http/http.dart' as http; -import 'dart:convert'; -import 'package:intl/intl.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; - -class CreateAssignmentPage extends StatefulWidget { - GoogleClassroomApi _googleClassroomApi = GoogleClassroomApi(); - - @override - _CreateAssignmentPageState createState() => _CreateAssignmentPageState(); -} - -class _CreateAssignmentPageState extends State { - final _formKey = GlobalKey(); - String? _selectedCourseId; - int? _points; - DateTime? _dueDate; - TimeOfDay? _dueTime; - final String _topic = 'Essay'; // Made static with fixed value - String? _title; - String? _instructions; - List _courses = []; - bool _isLoading = false; - bool _isSubmitting = false; - final MainController _controller = MainController(); - - @override - void initState() { - super.initState(); - _fetchCourses(); - } - - Future _getToken() async { - final token = LocalStorageService.getGoogleAccessToken(); - if (token == null) { - print('Error: No valid OAuth token. Ensure the required scopes are enabled.'); - } - return token; - } - - Future _fetchCourses() async { - setState(() => _isLoading = true); - final token = await _getToken(); - if (token == null) return; - - final response = await http.get( - Uri.parse('https://classroom.googleapis.com/v1/courses'), - headers: {'Authorization': 'Bearer $token'}, - ); - if (response.statusCode == 200) { - setState(() => _courses = jsonDecode(response.body)['courses'] ?? []); - } else { - print('Courses fetch error: ${response.statusCode}'); - } - setState(() => _isLoading = false); - } - - Future _selectDueDate(BuildContext context) async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime.now(), - lastDate: DateTime(2101), - ); - if (picked != null) { - setState(() { - _dueDate = picked; - }); - } - } - - Future _selectDueTime(BuildContext context) async { - final TimeOfDay? picked = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (picked != null) { - setState(() { - _dueTime = picked; - }); - } - } - - Future _createAssignment() async { - if (_formKey.currentState!.validate()) { - _formKey.currentState!.save(); - - if ((_dueDate != null && _dueTime == null) || - (_dueDate == null && _dueTime != null)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Please select both due date and time.'), - backgroundColor: Colors.red, - ), - ); - return; - } - - setState(() => _isSubmitting = true); - - final token = await _getToken(); - if (token == null) { - setState(() => _isSubmitting = false); - return; - } - - final url = Uri.parse( - 'https://classroom.googleapis.com/v1/courses/$_selectedCourseId/courseWork'); - final headers = { - 'Authorization': 'Bearer $token', - 'Content-Type': 'application/json', - }; - - Map requestBody = { - 'title': _title, - 'description': _instructions, - 'state': 'PUBLISHED', - 'workType': 'ASSIGNMENT', - 'maxPoints': _points, - }; - - if (_dueDate != null && _dueTime != null) { - requestBody['dueDate'] = { - 'year': _dueDate!.year, - 'month': _dueDate!.month, - 'day': _dueDate!.day, - }; - requestBody['dueTime'] = { - 'hours': _dueTime!.hour, - 'minutes': _dueTime!.minute, - 'seconds': 0, - }; - } - - String? topicIdNew = await widget._googleClassroomApi.getTopicId(_selectedCourseId!, _topic); - if (topicIdNew != null) { - requestBody['topicId'] = topicIdNew; - } - - final body = jsonEncode(requestBody); - - try { - final response = await http.post(url, headers: headers, body: body); - - if (response.statusCode == 200) { - print('Assignment created successfully!'); - Navigator.pop(context); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error creating assignment: ${response.statusCode}'), - backgroundColor: Colors.red, - ), - ); - } - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Network error: ${e.toString()}'), - backgroundColor: Colors.red, - ), - ); - } finally { - setState(() => _isSubmitting = false); - } - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: 'Create Assignment', - userprofileurl: LmsFactory.getLmsService().profileImage ?? '', - ), - body: _isLoading - ? Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Card( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DropdownButtonFormField( - decoration: InputDecoration( - labelText: 'Course', - border: OutlineInputBorder(), - ), - value: _selectedCourseId, - items: _courses.map((course) { - return DropdownMenuItem( - value: course['id'], - child: Text(course['name'] ?? 'Unnamed Course'), - ); - }).toList(), - onChanged: (value) { - setState(() => _selectedCourseId = value); - }, - validator: (value) => - value == null ? 'Please select a course' : null, - onSaved: (value) => _selectedCourseId = value, - ), - SizedBox(height: 16), - TextFormField( - decoration: InputDecoration( - labelText: 'Title', - border: OutlineInputBorder(), - ), - validator: (value) => - value!.isEmpty ? 'Please enter a title' : null, - onSaved: (value) => _title = value, - ), - SizedBox(height: 16), - TextFormField( - decoration: InputDecoration( - labelText: 'Instructions', - border: OutlineInputBorder(), - ), - maxLines: 3, - onSaved: (value) => _instructions = value, - ), - SizedBox(height: 16), - TextFormField( - decoration: InputDecoration( - labelText: 'Points', - border: OutlineInputBorder(), - ), - keyboardType: TextInputType.number, - validator: (value) { - if (value!.isEmpty) return 'Please enter points'; - if (int.tryParse(value) == null) - return 'Please enter a valid number'; - return null; - }, - onSaved: (value) => _points = int.parse(value!), - ), - SizedBox(height: 16), - ListTile( - title: Text(_dueDate == null - ? 'Select Due Date' - : 'Due Date: ${DateFormat('yyyy-MM-dd').format(_dueDate!)}'), - trailing: Icon(Icons.calendar_today), - onTap: () => _selectDueDate(context), - ), - ListTile( - title: Text(_dueTime == null - ? 'Select Due Time' - : 'Due Time: ${_dueTime!.format(context)}'), - trailing: Icon(Icons.access_time), - onTap: () => _selectDueTime(context), - ), - SizedBox(height: 16), - Text( - 'Assessment Type: $_topic', - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), - ), - SizedBox(height: 24), - ElevatedButton( - onPressed: _isSubmitting ? null : _createAssignment, - style: ElevatedButton.styleFrom( - minimumSize: Size(double.infinity, 50), - ), - child: _isSubmitting - ? SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ) - : Text('Create Assignment'), - ), - ], - ), - ), - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/Views/g_lesson_plan.dart b/team_a/teamA/lib/Views/g_lesson_plan.dart deleted file mode 100644 index 1c96a21a..00000000 --- a/team_a/teamA/lib/Views/g_lesson_plan.dart +++ /dev/null @@ -1,725 +0,0 @@ -// This file contains the code for the Google Lesson Plans page. -// This page allows users to create, edit, and delete lesson plans for Google Classroom courses. -// Users can select a course, grade level, and enter lesson plan details manually or generate them with AI. -import "dart:convert"; -import "package:flutter/material.dart"; -import "package:http/http.dart" as http; -import "package:learninglens_app/Api/llm/llm_api_modules_base.dart"; -import "package:learninglens_app/Api/lms/constants/learning_lens.constants.dart"; -import "package:learninglens_app/Api/lms/factory/lms_factory.dart"; -import "package:learninglens_app/Api/lms/google_classroom/google_classroom_api.dart"; -import "package:learninglens_app/Controller/custom_appbar.dart"; -import "package:learninglens_app/beans/course.dart"; -import "package:learninglens_app/services/local_storage_service.dart"; -import 'package:learninglens_app/Api/llm/openai_api.dart'; -import 'package:learninglens_app/Api/llm/grok_api.dart'; -import 'package:learninglens_app/Api/llm/perplexity_api.dart'; -import 'package:logging/logging.dart'; - -class GoogleLessonPlans extends StatefulWidget { - @override - State createState() => _LessonPlanState(); -} - -class _LessonPlanState extends State { - List? courses = []; - String? selectedCourse; - List courseworkMaterials = []; - dynamic selectedMaterial; - bool isEditing = false; - bool useAiGeneration = false; - String? selectedLLM; - String? selectedGradeLevel; - - // Loading flags - bool isLoadingCourses = false; - bool isLoadingLessonPlans = false; // New loading flag for lesson plans - bool isGeneratingAI = false; - bool isSaving = false; - bool isDeleting = false; - bool isUpdating = false; - - // API key availability flags - bool hasOpenAIKey = false; - bool hasGrokKey = false; - bool hasPerplexityKey = false; - - final TextEditingController lessonPlanNameController = - TextEditingController(); - final TextEditingController manualEntryController = TextEditingController(); - final TextEditingController aiPromptDetailsController = - TextEditingController(); - final log = Logger('GoogleLessonPlans'); - final GoogleClassroomApi googleClassroomApi = GoogleClassroomApi(); - - @override - void dispose() { - lessonPlanNameController.dispose(); - manualEntryController.dispose(); - aiPromptDetailsController.dispose(); - super.dispose(); - } - - @override - void initState() { - super.initState(); - _loadCourses(); - _checkApiKeys(); - } - - Future _checkApiKeys() async { - setState(() { - String openAIKey = LocalStorageService.getOpenAIKey() ?? ''; - String grokKey = LocalStorageService.getGrokKey() ?? ''; - String perplexityKey = LocalStorageService.getPerplexityKey() ?? ''; - - hasOpenAIKey = openAIKey.isNotEmpty; - hasGrokKey = grokKey.isNotEmpty; - hasPerplexityKey = perplexityKey.isNotEmpty; - // print('OpenAI key: $hasOpenAIKey'); - // hasGrokKey = LocalStorageService.getGrokKey() as bool; - // print('Grok key: $hasGrokKey'); - // hasPerplexityKey = LocalStorageService.getPerplexityKey() as bool; - // print('Perplexity key: $hasPerplexityKey'); - }); - } - - Future _getToken() async { - final token = LocalStorageService.getGoogleAccessToken(); - if (token == null) { - log.severe('Error: No valid OAuth token.'); - } - return token; - } - - Future _loadCourses() async { - setState(() { - isLoadingCourses = true; - }); - try { - List fetchedCourses = - await LmsFactory.getLmsService().getUserCourses(); - setState(() { - courses = fetchedCourses; - selectedCourse = null; - }); - } catch (e) { - log.severe('Failed to load courses: $e'); - } finally { - setState(() { - isLoadingCourses = false; - }); - } - } - - Future _loadLessonPlan(String courseId) async { - setState(() { - isLoadingLessonPlans = true; // Start loading indicator - }); - try { - final accessToken = await _getToken(); - if (accessToken == null) return; - - final url = Uri.parse( - 'https://classroom.googleapis.com/v1/courses/$courseId/courseWorkMaterials'); - final headers = { - 'Authorization': 'Bearer $accessToken', - 'Content-Type': 'application/json' - }; - final response = await http.get(url, headers: headers); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - setState(() { - courseworkMaterials = data['courseWorkMaterial'] ?? []; - selectedMaterial = null; - isEditing = false; - }); - } else { - print('Error loading coursework materials: ${response.statusCode}'); - setState(() { - courseworkMaterials = []; - }); - } - } catch (e) { - print('Error loading coursework materials: $e'); - setState(() { - courseworkMaterials = []; - }); - } finally { - setState(() { - isLoadingLessonPlans = false; // Stop loading indicator - }); - } - } - - Future generateLessonPlanWithAI() async { - setState(() { - isGeneratingAI = true; - }); - - final openApiKey = LocalStorageService.getOpenAIKey(); - final grokApiKey = LocalStorageService.getGrokKey(); - final perplexityApiKey = LocalStorageService.getPerplexityKey(); - - try { - late final LLM aiModel; - switch (selectedLLM) { - case 'ChatGPT': - aiModel = OpenAiLLM(openApiKey); - break; - case 'Grok': - aiModel = GrokLLM(grokApiKey); - break; - case 'Perplexity': - aiModel = PerplexityLLM(perplexityApiKey); - break; - default: - throw Exception('Unsupported AI model: $selectedLLM'); - } - - 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."; - - var result = await aiModel.generate(prompt); - setState(() { - manualEntryController.text = result; - }); - } catch (e) { - log.severe("Error generating lesson plan: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to generate lesson plan: $e')), - ); - } finally { - setState(() { - isGeneratingAI = false; - }); - } - } - - void _showLessonPlanDialog(dynamic lessonPlan) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text(lessonPlan['title'] ?? 'Untitled'), - content: SingleChildScrollView( - child: Container( - width: MediaQuery.of(context).size.width * 0.4, - child: Text(lessonPlan['description'] ?? '', - textAlign: TextAlign.left), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text("Close"), - ), - ], - ); - }, - ); - } - - void _clearFormFields() { - lessonPlanNameController.clear(); - manualEntryController.clear(); - aiPromptDetailsController.clear(); - setState(() { - selectedGradeLevel = null; - useAiGeneration = false; - selectedLLM = null; - isEditing = false; - selectedMaterial = null; - }); - } - - bool _canSubmit() { - return selectedCourse != null && - selectedGradeLevel != null && - lessonPlanNameController.text.isNotEmpty && - manualEntryController.text.isNotEmpty; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: 'Lesson Plans', - onRefresh: _loadCourses, - userprofileurl: LmsFactory.getLmsService().profileImage ?? '', - ), - body: isLoadingCourses - ? Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 2, - child: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Add New Lesson Plan', - style: TextStyle( - fontSize: 20, fontWeight: FontWeight.bold)), - SizedBox(height: 20), - DropdownButtonFormField( - value: selectedCourse, - items: [ - DropdownMenuItem( - value: null, - child: Text('Select Course'), - ), - ...(courses - ?.map>((course) { - return DropdownMenuItem( - value: course.id.toString(), - child: Text(course.fullName), - ); - }).toList() ?? - []) - ], - onChanged: (value) async { - setState(() { - selectedCourse = value; - }); - if (value != null) { - await _loadLessonPlan(value); - } - }, - decoration: InputDecoration( - labelText: 'Course', - border: OutlineInputBorder()), - ), - SizedBox(height: 20), - DropdownButtonFormField( - value: selectedGradeLevel, - items: LearningLensConstants.gradeLevels - .map>((grade) { - return DropdownMenuItem( - value: grade, child: Text(grade)); - }).toList(), - onChanged: (value) { - setState(() { - selectedGradeLevel = value; - }); - }, - decoration: InputDecoration( - labelText: 'Grade Level', - border: OutlineInputBorder(), - errorText: selectedGradeLevel == null && isSaving - ? 'Grade Level is required' - : null, - ), - validator: (value) => - value == null ? 'Required' : null, - ), - SizedBox(height: 20), - TextField( - controller: lessonPlanNameController, - decoration: InputDecoration( - labelText: 'Lesson Plan Name', - border: OutlineInputBorder(), - errorText: - lessonPlanNameController.text.isEmpty && - (isSaving || isGeneratingAI) - ? 'Lesson Plan Name is required' - : null, - ), - onChanged: (value) => setState(() {}), - ), - SizedBox(height: 20), - CheckboxListTile( - title: Text("Generate Lesson Plan with AI"), - value: useAiGeneration, - onChanged: (bool? value) { - setState(() { - useAiGeneration = value!; - if (!useAiGeneration) { - selectedLLM = null; - manualEntryController.clear(); - aiPromptDetailsController.clear(); - } - }); - }, - ), - if (useAiGeneration) - Column( - children: [ - IgnorePointer( - ignoring: !(hasOpenAIKey || - hasGrokKey || - hasPerplexityKey), - child: DropdownButtonFormField( - value: selectedLLM, - decoration: InputDecoration( - labelText: "Select AI Model", - border: OutlineInputBorder(), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: (hasOpenAIKey || - hasGrokKey || - hasPerplexityKey) - ? Colors.grey - : Colors.grey.withOpacity(0.3), - ), - ), - ), - onChanged: (String? newValue) { - if (newValue != null) { - setState(() => selectedLLM = newValue); - } - }, - items: [ - DropdownMenuItem( - value: 'ChatGPT', - enabled: hasOpenAIKey, - child: Text( - 'ChatGPT${!hasOpenAIKey ? " (API Key Required)" : ""}', - style: TextStyle( - color: hasOpenAIKey - ? null - : Colors.grey, - ), - ), - ), - DropdownMenuItem( - value: 'Grok', - enabled: hasGrokKey, - child: Text( - 'Grok${!hasGrokKey ? " (API Key Required)" : ""}', - style: TextStyle( - color: - hasGrokKey ? null : Colors.grey, - ), - ), - ), - DropdownMenuItem( - value: 'Perplexity', - enabled: hasPerplexityKey, - child: Text( - 'Perplexity${!hasPerplexityKey ? " (API Key Required)" : ""}', - style: TextStyle( - color: hasPerplexityKey - ? null - : Colors.grey, - ), - ), - ), - ], - ), - ), - if (!hasOpenAIKey && - !hasGrokKey && - !hasPerplexityKey) - Padding( - padding: EdgeInsets.only(top: 8.0), - child: Text( - 'Please add an API key in settings to use AI generation', - style: TextStyle( - color: Colors.red, fontSize: 12), - ), - ), - SizedBox(height: 20), - TextField( - controller: aiPromptDetailsController, - maxLines: 3, - decoration: InputDecoration( - labelText: - 'Additional Details for AI Prompt', - border: OutlineInputBorder(), - ), - ), - SizedBox(height: 20), - ElevatedButton( - onPressed: selectedLLM != null && - (selectedLLM == 'ChatGPT' && - hasOpenAIKey || - selectedLLM == 'Grok' && - hasGrokKey || - selectedLLM == 'Perplexity' && - hasPerplexityKey) && - selectedGradeLevel != null && - lessonPlanNameController - .text.isNotEmpty - ? () async { - await generateLessonPlanWithAI(); - } - : null, - child: isGeneratingAI - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2), - ) - : Text('Generate Lesson Plan'), - ), - ], - ), - SizedBox(height: 20), - TextField( - controller: manualEntryController, - maxLines: 8, - decoration: InputDecoration( - labelText: 'Enter/Edit Lesson Plan', - border: OutlineInputBorder(), - errorText: - manualEntryController.text.isEmpty && isSaving - ? 'Lesson Plan details are required' - : null, - ), - onChanged: (value) => setState(() {}), - ), - SizedBox(height: 20), - ElevatedButton( - onPressed: _canSubmit() - ? () async { - setState(() { - isSaving = true; - }); - String? materialId = - await googleClassroomApi - .createCourseWorkMaterial( - selectedCourse!, - lessonPlanNameController.text, - manualEntryController.text, - "https://example.com", - ); - if (materialId != null) { - await _loadLessonPlan(selectedCourse!); - _clearFormFields(); - } - setState(() { - isSaving = false; - }); - } - : null, - child: isSaving - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2), - ) - : Text('Submit'), - ), - ], - ), - ), - ), - Expanded( - flex: 3, - child: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Existing Lesson Plans', - style: TextStyle( - fontSize: 20, fontWeight: FontWeight.bold)), - SizedBox(height: 20), - isLoadingLessonPlans - ? Center(child: CircularProgressIndicator()) - : Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8.0)), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: DataTable( - columnSpacing: 20, - columns: [ - DataColumn( - label: SizedBox( - width: 150, - child: Text('Name', - style: TextStyle( - fontWeight: - FontWeight.bold)))), - DataColumn( - label: SizedBox( - width: 300, - child: Text('Lesson Plan', - style: TextStyle( - fontWeight: - FontWeight.bold)))), - DataColumn( - label: Text('Select', - style: TextStyle( - fontWeight: - FontWeight.bold))), - DataColumn( - label: Text('View', - style: TextStyle( - fontWeight: - FontWeight.bold))), - ], - rows: courseworkMaterials.map((material) { - bool isSelected = - selectedMaterial == material; - return DataRow( - cells: [ - DataCell( - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: 150), - child: Text( - material['title'] ?? - 'Untitled', - overflow: - TextOverflow.ellipsis), - ), - ), - DataCell( - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: 300), - child: SingleChildScrollView( - scrollDirection: - Axis.vertical, - child: Text( - material['description'] ?? - '', - maxLines: 3, - overflow: TextOverflow - .ellipsis), - ), - ), - ), - DataCell(Checkbox( - value: isSelected, - onChanged: (bool? selected) { - setState(() { - selectedMaterial = selected! - ? material - : null; - if (selectedMaterial != - null) { - lessonPlanNameController - .text = - selectedMaterial[ - 'title'] ?? - ''; - manualEntryController.text = - selectedMaterial[ - 'description'] ?? - ''; - isEditing = false; - } else { - _clearFormFields(); - } - }); - }, - )), - DataCell(ElevatedButton( - onPressed: () => - _showLessonPlanDialog( - material), - child: Text("View"), - )), - ], - ); - }).toList(), - ), - ), - ), - SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton( - onPressed: selectedMaterial != null - ? () async { - setState(() { - isDeleting = true; - }); - await googleClassroomApi - .deleteCourseWorkMaterial( - selectedCourse!, - selectedMaterial['id']); - await _loadLessonPlan(selectedCourse!); - _clearFormFields(); - setState(() { - isDeleting = false; - }); - } - : null, - child: isDeleting - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2), - ) - : Text('Delete Selected'), - ), - ElevatedButton( - onPressed: selectedMaterial != null - ? () => setState(() { - isEditing = true; - lessonPlanNameController.text = - selectedMaterial['title'] ?? ''; - manualEntryController.text = - selectedMaterial['description'] ?? - ''; - }) - : null, - child: Text('Edit Selected'), - ), - ElevatedButton( - onPressed: isEditing && selectedMaterial != null - ? () async { - setState(() { - isUpdating = true; - }); - try { - await googleClassroomApi - .updateCourseWorkMaterial( - selectedCourse!, - selectedMaterial['id'], - lessonPlanNameController.text, - manualEntryController.text, - ); - await _loadLessonPlan( - selectedCourse!); - _clearFormFields(); - } catch (e) { - print('Error during update: $e'); - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: Text( - 'Failed to update lesson plan: $e')), - ); - } finally { - setState(() { - isUpdating = false; - }); - } - } - : null, - child: isUpdating - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2), - ) - : Text('Save Update'), - ), - ], - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/team_a/teamA/lib/Views/g_quiz_question_page.dart b/team_a/teamA/lib/Views/g_quiz_question_page.dart deleted file mode 100644 index 7e8f7af9..00000000 --- a/team_a/teamA/lib/Views/g_quiz_question_page.dart +++ /dev/null @@ -1,300 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:learninglens_app/Api/lms/google_classroom/google_lms_service.dart'; -import 'package:learninglens_app/Controller/custom_appbar.dart'; -import 'package:learninglens_app/beans/g_question_form_data.dart'; -import 'dart:convert'; -import 'package:learninglens_app/services/local_storage_service.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:flutter/services.dart'; // Added for clipboard functionality - -// Dynamic form widget with table inside a card -class DynamicForm extends StatelessWidget { - final FormData formData; - - const DynamicForm({super.key, required this.formData}); - - // Determine question type based on options - String _getQuestionType(List options) { - if (options.isEmpty) { - return 'Short Answer'; - } else if (options.length == 2) { - return 'True/False'; - } else { - return 'Multiple Choice'; - } - } - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Title - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: Text( - formData.title, - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), - ), - ), - // Card containing assignment details and table - Card( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Assignment Details - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Start Date: ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - Text(formData.startDate ?? 'N/A'), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'End Date: ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - Text(formData.endDate ?? 'N/A'), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Form URL: ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - GestureDetector( - onTap: () async { - if (formData.formUrl != null) { - final Uri url = Uri.parse(formData.formUrl!); - if (await canLaunchUrl(url)) { - await launchUrl(url); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Cannot launch URL')), - ); - } - } - }, - child: Text( - formData.formUrl ?? 'N/A', - style: const TextStyle( - color: Colors.blue, - decoration: TextDecoration.underline, - ), - ), - ), - if (formData.formUrl != null) ...[ - IconButton( - icon: const Icon(Icons.copy), - onPressed: () { - Clipboard.setData( - ClipboardData(text: formData.formUrl!)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('URL copied to clipboard')), - ); - }, - ), - ], - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Status: ', - style: TextStyle(fontWeight: FontWeight.bold), - ), - Text(formData.status ?? 'N/A'), - ], - ), - const SizedBox(height: 16), - // Table - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: DataTable( - columnSpacing: 20, - dataRowHeight: 60, - headingRowColor: MaterialStateProperty.all( - Colors.blueAccent.withOpacity(0.1)), - border: TableBorder.all( - color: Colors.grey, - width: 1.0, - ), - columns: const [ - DataColumn( - label: Center( - child: Text( - 'Question No.', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - DataColumn( - label: Center( - child: Text( - 'Question', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - DataColumn( - label: Center( - child: Text( - 'Type', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - DataColumn( - label: Center( - child: Text( - 'Options', - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), - ], - rows: formData.questions.asMap().entries.map((entry) { - int index = entry.key + 1; - QuestionData questionData = entry.value; - return DataRow(cells: [ - DataCell(Text('$index')), - DataCell( - SizedBox( - width: 400, - child: Text( - questionData.question, - softWrap: true, - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - ), - ), - DataCell( - Text( - _getQuestionType(questionData.options), - ), - ), - DataCell( - SizedBox( - width: 300, - child: Text( - questionData.options.isEmpty - ? 'N/A' - : questionData.options.join(', '), - softWrap: true, - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - ), - ), - ]); - }).toList(), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } -} - -// Updated QuizQuestionPage -class QuizQuestionPage extends StatefulWidget { - final String coursedId; - final String assessmentId; - - const QuizQuestionPage( - {super.key, required this.coursedId, required this.assessmentId}); - - @override - State createState() => _QuizQuestionPageState(); -} - -class _QuizQuestionPageState extends State { - late Future _formDataFuture; - - @override - void initState() { - super.initState(); - print( - 'Course ID from QuizQuestion Page: ${widget.coursedId}, Assessment ID from QuizQuestion Page: ${widget.assessmentId}'); - GoogleLmsService googleLmsService = GoogleLmsService(); - _formDataFuture = googleLmsService.getAssignmentFormQuestions( - widget.coursedId, widget.assessmentId); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: 'Quiz Questions Here ....', - userprofileurl: LmsFactory.getLmsService().profileImage ?? '', - ), - body: FutureBuilder( - future: _formDataFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error_outline, color: Colors.red, size: 60), - const SizedBox(height: 16), - Text( - 'Error: ${snapshot.error.toString()}', - style: const TextStyle(fontSize: 18, color: Colors.red), - textAlign: TextAlign.center, - ), - ], - ), - ); - } else if (snapshot.hasData && snapshot.data!.questions.isNotEmpty) { - return DynamicForm(formData: snapshot.data!); - } else { - return const Center( - child: Text( - 'No questions found in the Google Form.', - style: TextStyle(fontSize: 18, color: Colors.grey), - ), - ); - } - }, - ), - ); - } -} diff --git a/team_a/teamA/lib/Views/iep_page.dart b/team_a/teamA/lib/Views/iep_page.dart deleted file mode 100644 index 9c5b182d..00000000 --- a/team_a/teamA/lib/Views/iep_page.dart +++ /dev/null @@ -1,681 +0,0 @@ -import "package:flutter/material.dart"; -import 'package:intl/intl.dart'; -import "package:learninglens_app/Api/lms/factory/lms_factory.dart"; -import "package:learninglens_app/Api/lms/moodle/moodle_lms_service.dart"; -import "package:learninglens_app/Controller/custom_appbar.dart"; -import 'package:learninglens_app/beans/course.dart'; -import 'package:learninglens_app/beans/participant.dart'; -import 'package:learninglens_app/beans/quiz.dart'; -import 'package:learninglens_app/beans/assignment.dart'; -import 'package:learninglens_app/beans/quiz_override'; -import 'package:learninglens_app/beans/override.dart'; - -class IepPage extends StatefulWidget { - IepPage(); - - @override - State createState() { - return _IepPageState(); - } -} - -class _IepPageState extends State { - TextEditingController _dateController = TextEditingController(); - TextEditingController _cutoffDateController = TextEditingController(); - bool? isChecked1 = false; - bool? isChecked2 = false; - String? selectedCourse; - String? selectedAssignment; - String? selectedEssay; - int? essayId; - int? quizId; - int? userId; - int? newEndTime; - String selectedDate = 'Select a Date'; - Future>? participants; - Future>? essay; - Future>? quiz; - List type = ['Quiz', 'Essay']; - double? epochTime; - double? epochTime2; - List attempts = ['Unlimited', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10']; - int? selectedAttempt; - - void _selectDate(BuildContext context) async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime(2000), - lastDate: DateTime(2100), - ); - if (picked != null && picked != DateTime.now()) { - setState(() { - _dateController.text = "${picked.toLocal()}".split(' ')[0]; - epochTime = picked.millisecondsSinceEpoch / 1000.round(); - }); - } - } - - void _selectCutOffDate(BuildContext context) async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime(2000), - lastDate: DateTime(2100), - ); - if (picked != null && picked != DateTime.now()) { - setState(() { - _cutoffDateController.text = "${picked.toLocal()}".split(' ')[0]; - epochTime2 = picked.millisecondsSinceEpoch / 1000.round(); - }); - } - } - - // void _getAssignmentOverride() async { ***** Not used ***** - // await MoodleLmsService().getAssignmentOverrides(); - // } - - // Function to show details in a dialog - void _showDetailsDialog(BuildContext context, Override override) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text("Details"), - content: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Student Name: ${override.fullname}"), - Text("Course Name: ${override.courseName}"), - Text("Assignment: ${override.type}: ${override.assignmentName}"), - Text("Extended Due Date: ${formatDate(override.endTime?.toString())}"), - Text("Cut Off Date: ${formatDate(override.cutoffTime?.toString())}"), - Text("Attempts: ${override.attempts?.toString() ?? 'N/A'}"), - ], - ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text("Close"), - ), - ], - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - final screenWidth = MediaQuery.of(context).size.width; - - return Scaffold( - appBar: CustomAppBar( - title: 'Individual Education Plans', - onRefresh: () { - // _loadCourses(); - }, - userprofileurl: LmsFactory.getLmsService().profileImage ?? '', - ), - body: SingleChildScrollView( - child: LayoutBuilder( - builder: (context, constraints) { - return Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Individual Education Plan Page', - style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold), - ), - Text( - 'Enroll Student in New IEP', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - DropdownMenu( - label: Text('Course'), - // helperText: 'Course', - hintText: 'Select Course', - width: 350, - dropdownMenuEntries: (getAllCourses() ?? []).map((Course course) { - return DropdownMenuEntry( - value: course.id.toString(), - label: course.fullName, - ); - }).toList(), - onSelected: (String? selectedValue) { - setState(() { - selectedCourse = selectedValue; - }); - participants = handleSelection(selectedValue); - if (selectedValue != null) { - essay = handleEssaySelection(int.parse(selectedValue)); - } else { - print('Selected Value is Null'); - } - if (selectedValue != null) { - quiz = handleQuizSelection(int.parse(selectedValue)); - } else { - print('Selected Value is Null'); - } - }, - ), - SizedBox(height: 10), - Visibility( - visible: selectedCourse != null, - child: FutureBuilder>( - future: participants, - builder: (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return CircularProgressIndicator(); - } else if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } else if (snapshot.hasData) { - List> dropdownEntries = snapshot.data!.map((Participant participant) { - return DropdownMenuEntry( - value: participant.id.toString(), - label: participant.fullname, - ); - }).toList(); - return DropdownMenu( - label: Text('Participants'), - // helperText: 'Participants', - hintText: 'Select Participants', - width: 350, - dropdownMenuEntries: dropdownEntries, - onSelected: (String? selectedParticipant) { - setState(() { - if (selectedParticipant != null) { - userId = int.parse(selectedParticipant); - } else { - print('No Participants were selected'); - } - }); - }, - ); - } else { - return DropdownMenu( - label: Text('Participants'), - hintText: 'Select A Course To View Participants', - // helperText: 'Participants', - width: 350, - dropdownMenuEntries: [], - ); - } - }, - ), - ), - SizedBox(height: 10), - Visibility( - visible: userId != null, - child: DropdownMenu( - width: 350, - label: Text('Assignment'), - // helperText: 'Assignment', - hintText: 'Select Quiz or Essay', - dropdownMenuEntries: type.map>((String value) { - return DropdownMenuEntry( - value: value, - label: value, - ); - }).toList(), - onSelected: (String? selectedValue) { - setState(() { - selectedAssignment = selectedValue; - essayId = null; - quizId = null; - _dateController.clear(); - _cutoffDateController.clear(); - epochTime = null; - epochTime2 = null; - }); - if (selectedAssignment == 'Essay') { - if (selectedCourse != null) { - handleEssaySelection(int.parse(selectedCourse!)); - } else { - print('Selected Course Is Null'); - } - } - }, - ), - ), - SizedBox(height: 10), - Visibility( - visible: selectedAssignment == 'Essay', - child: FutureBuilder>( - future: essay, - builder: (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return CircularProgressIndicator(); - } else if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } else if (snapshot.hasData) { - List> dropdownEntries = snapshot.data!.map((Assignment assignment) { - return DropdownMenuEntry( - value: assignment.id.toString(), - label: assignment.name, - ); - }).toList(); - return DropdownMenu( - label: Text('Essays'), - // helperText: 'Essays', - hintText: 'Select Essay', - width: 350, - dropdownMenuEntries: dropdownEntries, - onSelected: (String? selectedEssayId) { - setState(() { - if (selectedEssayId != null) { - essayId = int.parse(selectedEssayId); - } else { - print('Essay ID was Null'); - } - }); - }, - ); - } else { - return DropdownMenu( - hintText: 'Select A Course To View Essays', - label: Text('Essays'), - // helperText: 'Essays', - width: 350, - dropdownMenuEntries: [], - ); - } - }, - ), - ), - SizedBox(height: 10), - Visibility( - visible: selectedAssignment == 'Quiz', - child: FutureBuilder>( - future: quiz, - builder: (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return CircularProgressIndicator(); - } else if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } else if (snapshot.hasData) { - List> dropdownEntries = snapshot.data!.map((Quiz quiz) { - return DropdownMenuEntry( - value: quiz.id.toString(), - label: quiz.name!, - ); - }).toList(); - return DropdownMenu( - label: Text('Quiz'), - // helperText: 'Quiz', - hintText: 'Select Quiz', - width: 350, - dropdownMenuEntries: dropdownEntries, - onSelected: (String? selectedQuizId) { - setState(() { - if (selectedQuizId != null) { - quizId = int.parse(selectedQuizId); - } else { - print('Quiz Id is null'); - } - }); - }, - ); - } else { - return DropdownMenu( - hintText: 'Select A Course To View Quizzes', - label: Text('Quizzes'), - // helperText: 'Quizzes', - width: 350, - dropdownMenuEntries: [], - ); - } - }, - ), - ), - SizedBox(height: 10), - Visibility( - visible: quizId != null, - child: DropdownMenu( - width: 350, - label: Text('Attempts'), - // helperText: 'Attempts', - hintText: 'Select Number of Attempts', - dropdownMenuEntries: attempts.map>((String attempts) { - return DropdownMenuEntry( - value: attempts, - label: attempts, - ); - }).toList(), - onSelected: (String? selectedValue) { - setState(() { - if (selectedValue == 'Unlimited') { - selectedAttempt = 0; - } else { - selectedAttempt = int.parse(selectedValue!); - } - }); - }, - ), - ), - SizedBox(height: 10), - Visibility( - visible: selectedAttempt != null && selectedAssignment == 'Quiz', - child: Row( - children: [ - Container( - width: 250, - margin: EdgeInsets.only(right: 20), - padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black), - ), - child: Text( - _dateController.text, - style: TextStyle(fontSize: 20), - ), - ), - GestureDetector( - onTap: () => _selectDate(context), // Correct usage of named parameter `onTap` - child: Container( - padding: EdgeInsets.symmetric(vertical: 15, horizontal: 10), - decoration: BoxDecoration( - border: Border.all(color: Colors.black), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - 'Select Date', - ), - ), - ), - ], - ), - ), - // SizedBox(height: 10), - Visibility( - visible: essayId != null && selectedAssignment == 'Essay', - child: Column( - children: [ - Row( - children: [ - Container( - width: 250, - margin: EdgeInsets.only(right: 20), - padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black), - ), - child: Text( - _dateController.text, - style: TextStyle(fontSize: 20), - ), - ), - GestureDetector( - onTap: () => _selectDate(context), // Correct usage of named parameter `onTap` - child: Container( - padding: EdgeInsets.symmetric(vertical: 15, horizontal: 18), - decoration: BoxDecoration( - border: Border.all(color: Colors.black), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - 'Select Due Date', - ), - ), - ), - ], - ), - SizedBox(height: 20), - Row( - children: [ - Container( - width: 250, - margin: EdgeInsets.only(right: 20), - padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.black), - ), - child: Text( - _cutoffDateController.text, - style: TextStyle(fontSize: 20), - ), - ), - GestureDetector( - onTap: () => _selectCutOffDate(context), // Correct usage of named parameter `onTap` - child: Container( - padding: EdgeInsets.symmetric(vertical: 15, horizontal: 10), - decoration: BoxDecoration( - border: Border.all(color: Colors.black), - borderRadius: BorderRadius.circular(8), - ), - child: Text( - 'Select Cutoff Date', - ), - ), - ), - ], - ), - SizedBox(height: 10), - Text( - 'Cutoff Date: Last day it can be submitted late', - style: TextStyle(fontSize: 14, color: Colors.grey), - ), - ], - ), - ), - SizedBox(height: 10), - Visibility( - visible: epochTime != null, - child: Container( - padding: EdgeInsets.only(top: 50, left: 160), - child: ElevatedButton( - onPressed: () { - if (selectedAssignment == 'Quiz') { - quizOver(epochTime, quizId, userId, selectedAttempt); - Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => IepPage())); - } else if (selectedAssignment == 'Essay') { - essayOver(epochTime, essayId, userId, epochTime2); - Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => IepPage())); - } - }, - child: Text('Submit'), - ), - ), - ), - ], - ), - Expanded( - child: Column( - children: [ - Text( - 'Existing IEPs', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - Container( - margin: EdgeInsets.only(left: 10), - width: constraints.maxWidth * 0.7, - height: 830, - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(0.0), - ), - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: screenWidth < 1024 - ? _buildSimplifiedTable(context) - : _buildFullTable(context), - ), - ), - ), - ], - ), - ), - ], - ); - }, - ), - ), - ); - } - - // Build the simplified table for smaller screens - DataTable _buildSimplifiedTable(BuildContext context) { - return DataTable( - headingRowColor: MaterialStateProperty.all(const Color.fromARGB(255, 77, 195, 89)), - columns: [ - DataColumn(label: Text('Student Name', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16))), - DataColumn(label: Text('Course Name', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16))), - DataColumn(label: Text('Actions', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16))), - ], - rows: (getOverrides() ?? []).asMap().map((index, override) { - return MapEntry(index, DataRow( - cells: [ - DataCell(Text(override.fullname)), - DataCell(Text(override.courseName)), - DataCell( - ElevatedButton( - onPressed: () => _showDetailsDialog(context, override), - child: Text("View"), - ), - ), - ], - )); - }).values.toList(), - ); - } - - // Build the full table for larger screens - DataTable _buildFullTable(BuildContext context) { - return DataTable( - headingRowColor: MaterialStateProperty.all(const Color.fromARGB(255, 77, 195, 89)), - columns: [ - DataColumn(label: Text('Student Name', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16))), - DataColumn(label: Text('Course Name', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16))), - DataColumn(label: Text('Assignment', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16))), - DataColumn(label: Text('Due Dates', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16))), - DataColumn(label: Text('Attempts', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16))), - ], - rows: (getOverrides() ?? []).asMap().map((index, override) { - return MapEntry(index, buildDataRow(override, index)); - }).values.toList(), - ); - } -} - -List? getAllCourses() { - List? result; - result = MoodleLmsService().courses; - return result; -} - -Future>? getAllParticipants(String courseID) async { - List? participants; - participants = await MoodleLmsService().getCourseParticipants(courseID); - return participants; -} - -Future> handleSelection(String? courseID) async { - if (courseID != null) { - List? participants = await getAllParticipants(courseID); - if (participants == null) { - return []; - } else { - return participants; - } - } else { - print('Course ID was Null.'); - return []; - } -} - -Future> handleEssaySelection(int? courseID) async { - if (courseID != null) { - List? essays = await MoodleLmsService().getEssays(courseID); - if (essays.isNotEmpty) { - return essays; - } else { - return []; - } - } else { - return []; - } -} - -Future> handleQuizSelection(int? courseID) async { - if (courseID != null) { - List? quizzes = await MoodleLmsService().getQuizzes(courseID); - if (quizzes.isNotEmpty) { - return quizzes; - } else { - return []; - } - } else { - return []; - } -} - -void quizOver(epochTime, quizId, userId, attempts) async { - QuizOverride override = await MoodleLmsService().addQuizOverride(quizId: quizId, userId: userId, timeClose: epochTime, attempts: attempts); - print('Override: $override'); -} - -void essayOver(epochTime, essayId, userId, epochTime2) async { - String essayOverride = await MoodleLmsService().addEssayOverride(assignid: essayId, userId: userId, dueDate: epochTime, cutoffDate: epochTime2); - print('Override: $essayOverride'); -} - -List? getOverrides() { - List? overrides; - overrides = MoodleLmsService().overrides; - return overrides; -} - -String formatDate(String? dateString) { - if (dateString == null) { - return 'N/A'; - } - DateFormat dateFormat = DateFormat('MMM d yyyy hh:mm a'); - return dateFormat.format(DateTime.parse(dateString)); -} - -DataRow buildDataRow(Override override, int index) { - return DataRow( - color: MaterialStateProperty.resolveWith((states) { - return index % 2 == 0 ? Colors.grey[400]! : Colors.white; - }), - cells: [ - DataCell(Text(override.fullname)), - DataCell(Text(override.courseName)), - DataCell( - Text( - "${override.type}: ${override.assignmentName}", - style: TextStyle(fontWeight: FontWeight.bold), - ), - ), - DataCell( - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Extended: ${formatDate(override.endTime?.toString())}", - style: TextStyle(fontSize: 14), - ), - SizedBox(height: 4), - Text( - "Cut off: ${formatDate(override.cutoffTime?.toString())}", - style: TextStyle(fontSize: 14), - ), - ], - ), - ), - DataCell(Text(override.attempts?.toString() ?? 'N/A')), - ], - ); -} \ No newline at end of file diff --git a/team_a/teamA/lib/Views/lesson.dart b/team_a/teamA/lib/Views/lesson.dart deleted file mode 100644 index 93fb562a..00000000 --- a/team_a/teamA/lib/Views/lesson.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:learninglens_app/Api/lms/moodle/moodle_lms_service.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'dart:convert'; - -class Lesson { - final int courseId; - final String lessonPlanName; - final String? content; // Content is now explicitly the lesson description - - Lesson( - {required this.lessonPlanName, required this.courseId, this.content}); - - /// Convert lesson plan to JSON for storage or API submission - Map toJson() { - return { - 'lessonPlanName': lessonPlanName, - 'courseId': courseId, - 'content': content, // Pass content as lesson description - }; - } - - /// Save lesson plan locally using SharedPreferences - Future saveLessonLocally() async { - final prefs = await SharedPreferences.getInstance(); - final List savedPlans = prefs.getStringList('lessonPlans') ?? []; - savedPlans.add(jsonEncode(this.toJson())); - await prefs.setStringList('lessonPlans', savedPlans); - } - - /// Load lesson plans from SharedPreferences - static Future> loadLessonPlans() async { - final prefs = await SharedPreferences.getInstance(); - final List savedPlans = prefs.getStringList('lessonPlans') ?? []; - - return savedPlans.map((plan) { - final Map planJson = jsonDecode(plan); - return Lesson( - lessonPlanName: planJson['lessonPlanName'], - courseId: planJson['courseId'], - content: planJson['content'], - ); - }).toList(); - } - - /// Submit lesson plan and automatically create a Moodle lesson - Future submitLesson() async { - final lessonPlanJson = this.toJson(); - print("Submitting lesson plan with data: $lessonPlanJson"); - var courseId = lessonPlanJson['courseId']; - var lessonPlanName = lessonPlanJson['lessonPlanName']; - var content = lessonPlanJson['content']; - return await MoodleLmsService().createLesson( - courseId: courseId, // Pass courseId as a string - lessonPlanName: lessonPlanName, - content: content ?? "No description provided", - ); - } -} diff --git a/team_a/teamA/lib/Views/lesson_plans.dart b/team_a/teamA/lib/Views/lesson_plans.dart deleted file mode 100644 index 9b827732..00000000 --- a/team_a/teamA/lib/Views/lesson_plans.dart +++ /dev/null @@ -1,914 +0,0 @@ -import "package:flutter/material.dart"; -import "package:learninglens_app/Api/lms/constants/learning_lens.constants.dart"; -import "package:learninglens_app/Api/lms/factory/lms_factory.dart"; -import "package:learninglens_app/Api/lms/moodle/moodle_lms_service.dart"; -import "package:learninglens_app/Controller/custom_appbar.dart"; -import "package:learninglens_app/Views/lesson.dart"; -import "package:learninglens_app/beans/course.dart"; -import "package:learninglens_app/beans/lesson_plan.dart"; -import 'package:learninglens_app/Api/llm/enum/llm_enum.dart'; -import 'package:learninglens_app/Api/llm/openai_api.dart'; -import 'package:learninglens_app/Api/llm/grok_api.dart'; -import 'package:learninglens_app/Api/llm/perplexity_api.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; -import 'dart:convert'; - -// Define a constant for grade levels - -class LessonPlans extends StatefulWidget { - @override - State createState() => _LessonPlanState(); -} - -class _LessonPlanState extends State { - List? courses = []; - String? selectedCourse; - List lessonPlans = []; - LessonPlan? selectedLessonPlan; - bool isEditing = false; - bool useAiGeneration = false; - LlmType? selectedLLM; - bool isSubmitDisabled = false; - String? selectedGradeLevel; - bool isSubmitting = false; - - //final ScrollController _scrollController = ScrollController(); - List _scrollControllers = []; - - final TextEditingController lessonPlanNameController = TextEditingController(); - final TextEditingController manualEntryController = TextEditingController(); - final TextEditingController additionalPromptController = TextEditingController(); // New controller - bool showAiPromptSection = false; // New state variable to control visibility - bool isGeneratingLesson = false; // New state variable for loading state - - @override - void dispose() { - lessonPlanNameController.dispose(); - manualEntryController.dispose(); - additionalPromptController.dispose(); - for (var controller in _scrollControllers) { - controller.dispose(); - } - super.dispose(); - } - - @override - void initState() { - super.initState(); - _loadCourses(); // Load courses when the widget initializes - } - - Future _loadCourses() async { - try { - var userCourses = await LmsFactory.getLmsService().courses; - setState(() { - courses = userCourses; // Update the state with fetched courses - }); - } catch (e) { - print("Error loading courses: $e"); - } - } - - void _fetchLessonPlans(int courseId) async { - List plans = await MoodleLmsService().getLessonPlans(courseId); - setState(() { - lessonPlans = plans; - _scrollControllers = List.generate( - lessonPlans.length, - (index) => ScrollController(), - growable: true, - ); - }); - } - - String normalizeText(String text) { - return text - .replaceAll('’', "'") // Replace garbled curly apostrophe with a plain apostrophe - .replaceAll('’', "'") // Replace Unicode curly apostrophe with a plain apostrophe - .replaceAll('“', '"') // Replace Unicode left double quotation mark - .replaceAll('”', '"') // Replace Unicode right double quotation mark - .replaceAll('‘', "'") // Replace Unicode left single quotation mark - .replaceAll('’', "'"); // Replace Unicode right single quotation mark -} - - Future generateLessonPlanWithAI() async { - try { - final aiModel; - if (selectedLLM == LlmType.CHATGPT) { - aiModel = OpenAiLLM(LocalStorageService.getOpenAIKey()); - } else if (selectedLLM == LlmType.GROK) { - aiModel = GrokLLM(LocalStorageService.getGrokKey()); - } else { - aiModel = PerplexityLLM(LocalStorageService.getPerplexityKey()); - } - - String prompt = """Generate an all text (no diagrams) lesson of less than 500 words for ${lessonPlanNameController.text} for grade $selectedGradeLevel covering key topics like ${manualEntryController.text}. ${additionalPromptController.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.postToLlm(prompt); - - String normalizedText = utf8.decode(result.codeUnits); - - setState(() { - manualEntryController.text = normalizeText(normalizedText); // Update the manual entry textbox with the AI response - }); - } catch (e) { - print("Error generating lesson plan: $e"); - setState(() { - isGeneratingLesson = false; // Ensure loading spinner is hidden on error - }); - } -} - - String _convertTextToHtml(String text) { - return "

    ${text.replaceAll('\n\n', '

    ').replaceAll('\n', '
    ')}

    "; - } - - String _stripHtmlTags(String htmlText) { - return htmlText - .replaceAll(RegExp(r']*>'), '\n\n') - .replaceAll(RegExp(r'

    '), '') - .replaceAll(RegExp(r''), '\n') - .replaceAll(RegExp(r'<[^>]*>'), '') - .trim(); - } - - void _showLessonPlanDialog(LessonPlan plan) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text(plan.name), - content: SingleChildScrollView( - child: Container( - width: MediaQuery.of(context).size.width * 0.4, - child: Text(_stripHtmlTags(plan.intro), textAlign: TextAlign.left), - ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text("Close"), - ), - ], - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: 'Lesson Plans', - onRefresh: _loadCourses, // Refresh courses when the app bar is refreshed - userprofileurl: LmsFactory.getLmsService().profileImage ?? ''), - body: SingleChildScrollView( - child: LayoutBuilder( - builder: (context, constraints) { - bool isNarrowScreen = constraints.maxWidth <= 805; // Breakpoint for narrow screens - bool isMediumScreen = constraints.maxWidth > 805 && constraints.maxWidth <= 1170; // Breakpoint for medium screens - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Add New Lesson Plan Section - if (!isNarrowScreen) - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Add New Lesson Plan', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - SizedBox(height: 10), - DropdownButtonFormField( - value: selectedCourse, - items: courses?.map>((course) { - return DropdownMenuItem( - value: course.id.toString(), - child: Text(course.fullName), - ); - }).toList() ?? [], - onChanged: (value) { - setState(() { - selectedCourse = value; - _fetchLessonPlans(int.parse(value!)); - lessonPlanNameController.clear(); - manualEntryController.clear(); - additionalPromptController.clear(); - selectedLLM = null; - isEditing = false; - isSubmitDisabled = false; - useAiGeneration = false; - showAiPromptSection = false; - selectedGradeLevel = null; - }); - }, - decoration: InputDecoration( - labelText: 'Course', - border: OutlineInputBorder(), - ), - ), - SizedBox(height: 10), - TextField( - controller: lessonPlanNameController, - decoration: InputDecoration( - labelText: 'Lesson Plan Name', - border: OutlineInputBorder(), - ), - ), - SizedBox(height: 10), - DropdownButtonFormField( - decoration: const InputDecoration( - labelText: 'Select Grade Level', - border: OutlineInputBorder(), - ), - value: selectedGradeLevel, - items: LearningLensConstants.gradeLevels.map((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - onChanged: (String? newValue) { - setState(() { - selectedGradeLevel = newValue; - }); - }, - ), - SizedBox(height: 20), - - // AI and Manual Generation Options - // Inside the build method, where the AI generation options are defined - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CheckboxListTile( - title: Text("Generate Lesson Plan with AI"), - value: useAiGeneration, - onChanged: (bool? value) { - setState(() { - useAiGeneration = value!; - showAiPromptSection = useAiGeneration; // Show/hide the new section - if (!useAiGeneration) { - selectedLLM = null; // Reset dropdown when unchecked - } - }); - }, - ), - - if (useAiGeneration) - DropdownButtonFormField( - value: selectedLLM, - decoration: const InputDecoration(labelText: "Select AI Model"), - onChanged: (LlmType? newValue) { - setState(() { - selectedLLM = newValue; - }); - }, - items: LlmType.values.map((LlmType llm) { - return DropdownMenuItem( - value: llm, - enabled: LocalStorageService.userHasLlmKey(llm), - child: Text(llm.displayName, style: TextStyle( - color: LocalStorageService.userHasLlmKey(llm) ? Colors.black87 : Colors.grey, - )), - ); - }).toList(), - disabledHint: Text("Enable AI to select a model"), - ), - - // New UI elements for additional AI prompt - if (showAiPromptSection) - Column( - children: [ - SizedBox(height: 20), - TextField( - controller: additionalPromptController, - maxLines: 4, // Half the height of the manual entry textbox - decoration: InputDecoration( - labelText: "Enter any additional prompts for the AI model to customize your lesson", - border: OutlineInputBorder(), - ), - ), - SizedBox(height: 10), - ElevatedButton( - onPressed: isGeneratingLesson || selectedLLM == null - ? null - : () async { - setState(() { - isGeneratingLesson = true; // Show loading spinner - }); - - await generateLessonPlanWithAI(); // Call the AI generation logic - - setState(() { - isGeneratingLesson = false; // Hide loading spinner - }); - }, - child: isGeneratingLesson - ? SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : Text("Generate Lesson Plan"), - ), - ], - ), - ], - ), - SizedBox(height: 20), - - // Textbox for Lesson Plan Content - TextField( - controller: manualEntryController, - maxLines: 8, - decoration: InputDecoration( - labelText: useAiGeneration - ? "Edit the AI-generated lesson plan" - : "Enter Lesson Plan Manually", - border: OutlineInputBorder(), - ), - ), - SizedBox(height: 20), - - // Submit Button - ElevatedButton( - onPressed: isSubmitDisabled || isSubmitting - ? null - : () async { - setState(() { - isSubmitting = true; - }); - - if (selectedCourse != null) { - Lesson newLp = Lesson( - lessonPlanName: lessonPlanNameController.text, - courseId: int.parse(selectedCourse!), - content: _convertTextToHtml(manualEntryController.text), - ); - bool success = await newLp.submitLesson(); - print(success ? 'Lesson plan sent successfully' : 'Lesson plan send failed'); - _fetchLessonPlans(int.parse(selectedCourse!)); - lessonPlanNameController.clear(); - manualEntryController.clear(); - additionalPromptController.clear(); - selectedLLM = null; - useAiGeneration = false; - - } - - setState(() { - isSubmitting = false; - }); - }, - child: isSubmitting - ? SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : Text('Submit'), - ), - ], - ), - ), - SizedBox(width: 20), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Existing Lesson Plans', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - SizedBox(height: 10), - Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8.0), - ), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: DataTable( - columnSpacing: 10, - columns: [ - DataColumn( - label: SizedBox( - width: 100, - child: Text('Name', style: TextStyle(fontWeight: FontWeight.bold)), - ), - ), - if (!isMediumScreen) // Hide Lesson Plan column for medium screens - DataColumn( - label: SizedBox( - width: 200, - child: Text('Lesson Plan', style: TextStyle(fontWeight: FontWeight.bold)), - ), - ), - DataColumn( - label: Text('Select', style: TextStyle(fontWeight: FontWeight.bold)), - ), - DataColumn( - label: Text('View', style: TextStyle(fontWeight: FontWeight.bold)), - ), - ], - rows: lessonPlans.asMap().entries.map((entry) { - int index = entry.key; - LessonPlan plan = entry.value; - bool isSelected = selectedLessonPlan == plan; - return DataRow( - cells: [ - DataCell( - ConstrainedBox( - constraints: BoxConstraints(maxWidth: 150), - child: Text( - plan.name, - overflow: TextOverflow.ellipsis, - ), - ), - ), - if (!isMediumScreen) // Hide Lesson Plan column for medium screens - DataCell( - ConstrainedBox( - constraints: BoxConstraints(maxWidth: 250), - child: Container( - height: 60, // Fixed height to maintain row size - child: Scrollbar( - controller: _scrollControllers[index], // Add a ScrollController - child: SingleChildScrollView( - controller: _scrollControllers[index], // Add a ScrollController - scrollDirection: Axis.vertical, - child: Text( - _stripHtmlTags(plan.intro), - ), - ), - ), - ), - ), - ), - DataCell( - Checkbox( - value: isSelected, - onChanged: (bool? selected) { - setState(() { - selectedLessonPlan = selected! ? plan : null; - }); - }, - ), - ), - DataCell( - ElevatedButton( - onPressed: () { - _showLessonPlanDialog(plan); - }, - child: Text("View"), - ), - ), - ], - ); - }).toList(), - ), - ), - ), - SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton( - onPressed: selectedLessonPlan != null && !isEditing - ? () async { - await MoodleLmsService().deleteLessonPlan(selectedLessonPlan!.id); - _fetchLessonPlans(int.parse(selectedCourse!)); - setState(() { - selectedLessonPlan = null; - }); - } - : null, - child: Text('Delete Selected'), - ), - ElevatedButton( - onPressed: selectedLessonPlan != null && !isEditing - ? () { - setState(() { - isEditing = true; - lessonPlanNameController.text = selectedLessonPlan!.name; - manualEntryController.text = _stripHtmlTags(selectedLessonPlan!.intro); - isSubmitDisabled = true; - }); - } - : null, - child: Text('Edit Selected'), - ), - ElevatedButton( - onPressed: isEditing - ? () async { - String manualEntryToHtml = _convertTextToHtml(manualEntryController.text); - await MoodleLmsService().updateLessonPlan( - lessonId: selectedLessonPlan!.id, - name: lessonPlanNameController.text, - intro: manualEntryToHtml, - available: 1672531200, //dummy value - deadline: 1675132800, //dummy value - ); - _fetchLessonPlans(int.parse(selectedCourse!)); - lessonPlanNameController.clear(); - manualEntryController.clear(); - additionalPromptController.clear(); - selectedLLM = null; - useAiGeneration = false; - - setState(() { - selectedLessonPlan = null; - isEditing = false; - isSubmitDisabled = false; - }); - } - : null, - child: Text('Save Edits'), - ), - ], - ), - ], - ), - ), - ], - ), - - // For Narrow Screens (805px or less) - if (isNarrowScreen) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Add New Lesson Plan', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - SizedBox(height: 10), - DropdownButtonFormField( - value: selectedCourse, - items: courses?.map>((course) { - return DropdownMenuItem( - value: course.id.toString(), - child: Text(course.fullName), - ); - }).toList() ?? [], - onChanged: (value) { - setState(() { - selectedCourse = value; - _fetchLessonPlans(int.parse(value!)); - lessonPlanNameController.clear(); - manualEntryController.clear(); - additionalPromptController.clear(); - additionalPromptController.clear(); - selectedLLM = null; - isEditing = false; - isSubmitDisabled = false; - useAiGeneration = false; - showAiPromptSection = false; - selectedGradeLevel = null; - }); - }, - decoration: InputDecoration( - labelText: 'Course', - border: OutlineInputBorder(), - ), - ), - SizedBox(height: 10), - TextField( - controller: lessonPlanNameController, - decoration: InputDecoration( - labelText: 'Lesson Plan Name', - border: OutlineInputBorder(), - ), - ), - SizedBox(height: 10), - DropdownButtonFormField( - decoration: const InputDecoration( - labelText: 'Select Grade Level', - border: OutlineInputBorder(), - ), - value: selectedGradeLevel, - items: LearningLensConstants.gradeLevels.map((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - onChanged: (String? newValue) { - setState(() { - selectedGradeLevel = newValue; - }); - }, - ), - SizedBox(height: 20), - - // AI and Manual Generation Options - // Inside the build method, where the AI generation options are defined - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CheckboxListTile( - title: Text("Generate Lesson Plan with AI"), - value: useAiGeneration, - onChanged: (bool? value) { - setState(() { - useAiGeneration = value!; - showAiPromptSection = useAiGeneration; // Show/hide the new section - if (!useAiGeneration) { - selectedLLM = null; // Reset dropdown when unchecked - } - }); - }, - ), - - if (useAiGeneration) - DropdownButtonFormField( - value: selectedLLM, - decoration: const InputDecoration(labelText: "Select AI Model"), - onChanged: (LlmType? newValue) { - setState(() { - selectedLLM = newValue; - }); - }, - items: LlmType.values.map((LlmType llm) { - return DropdownMenuItem( - value: llm, - enabled: LocalStorageService.userHasLlmKey(llm), - child: Text(llm.displayName, style: TextStyle( - color: LocalStorageService.userHasLlmKey(llm) ? Colors.black87 : Colors.grey, - )), - ); - }).toList(), - disabledHint: Text("Enable AI to select a model"), - ), - - // New UI elements for additional AI prompt - if (showAiPromptSection) - Column( - children: [ - SizedBox(height: 20), - TextField( - controller: additionalPromptController, - maxLines: 4, // Half the height of the manual entry textbox - decoration: InputDecoration( - labelText: "Enter any additional prompts for the AI model to customize your lesson", - border: OutlineInputBorder(), - ), - ), - SizedBox(height: 10), - ElevatedButton( - onPressed: isGeneratingLesson || selectedLLM == null - ? null - : () async { - setState(() { - isGeneratingLesson = true; // Show loading spinner - }); - - await generateLessonPlanWithAI(); // Call the AI generation logic - - setState(() { - isGeneratingLesson = false; // Hide loading spinner - }); - }, - child: isGeneratingLesson - ? SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : Text("Generate Lesson Plan"), - ), - ], - ), - ], - ), - SizedBox(height: 20), - - // Textbox for Lesson Plan Content - TextField( - controller: manualEntryController, - maxLines: 8, - decoration: InputDecoration( - labelText: useAiGeneration - ? "Edit the AI-generated lesson plan" - : "Enter the Lesson Plan Manually", - border: OutlineInputBorder(), - ), - ), - SizedBox(height: 20), - - // Submit Button - ElevatedButton( - onPressed: isSubmitDisabled || isSubmitting - ? null - : () async { - setState(() { - isSubmitting = true; - }); - - if (selectedCourse != null) { - Lesson newLp = Lesson( - lessonPlanName: lessonPlanNameController.text, - courseId: int.parse(selectedCourse!), - content: _convertTextToHtml(manualEntryController.text), - ); - bool success = await newLp.submitLesson(); - print(success ? 'Lesson plan sent successfully' : 'Lesson plan send failed'); - _fetchLessonPlans(int.parse(selectedCourse!)); - lessonPlanNameController.clear(); - manualEntryController.clear(); - lessonPlanNameController.clear(); - manualEntryController.clear(); - additionalPromptController.clear(); - selectedLLM = null; - isEditing = false; - isSubmitDisabled = false; - useAiGeneration = false; - showAiPromptSection = false; - selectedGradeLevel = null; - } - - setState(() { - isSubmitting = false; - }); - }, - child: isSubmitting - ? SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : Text('Submit'), - ), - SizedBox(height: 20), - - // Existing Lesson Plans Section for Narrow Screens - Text('Existing Lesson Plans', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - SizedBox(height: 10), - Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8.0), - ), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: DataTable( - columnSpacing: 10, - columns: [ - DataColumn( - label: SizedBox( - width: 100, - child: Text('Name', style: TextStyle(fontWeight: FontWeight.bold)), - ), - ), - if (!isMediumScreen) // Hide Lesson Plan column for medium screens - DataColumn( - label: SizedBox( - width: 200, - child: Text('Lesson Plan', style: TextStyle(fontWeight: FontWeight.bold)), - ), - ), - DataColumn( - label: Text('Select', style: TextStyle(fontWeight: FontWeight.bold)), - ), - DataColumn( - label: Text('View', style: TextStyle(fontWeight: FontWeight.bold)), - ), - ], - rows: lessonPlans.asMap().entries.map((entry) { - int index = entry.key; - LessonPlan plan = entry.value; - bool isSelected = selectedLessonPlan == plan; - return DataRow( - cells: [ - DataCell( - ConstrainedBox( - constraints: BoxConstraints(maxWidth: 150), - child: Text( - plan.name, - overflow: TextOverflow.ellipsis, - ), - ), - ), - if (!isMediumScreen) // Hide Lesson Plan column for medium screens - DataCell( - ConstrainedBox( - constraints: BoxConstraints(maxWidth: 250), - child: Container( - height: 60, // Fixed height to maintain row size - child: Scrollbar( - controller: _scrollControllers[index], // Add a ScrollController - child: SingleChildScrollView( - controller: _scrollControllers[index], // Add a ScrollController - scrollDirection: Axis.vertical, - child: Text( - _stripHtmlTags(plan.intro), - ), - ), - ), - ), - ), - ), - DataCell( - Checkbox( - value: isSelected, - onChanged: (bool? selected) { - setState(() { - selectedLessonPlan = selected! ? plan : null; - }); - }, - ), - ), - DataCell( - ElevatedButton( - onPressed: () { - _showLessonPlanDialog(plan); - }, - child: Text("View"), - ), - ), - ], - ); - }).toList(), - ), - ), - ), - SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ElevatedButton( - onPressed: selectedLessonPlan != null && !isEditing - ? () async { - await MoodleLmsService().deleteLessonPlan(selectedLessonPlan!.id); - _fetchLessonPlans(int.parse(selectedCourse!)); - setState(() { - selectedLessonPlan = null; - }); - } - : null, - child: Text('Delete Selected'), - ), - ElevatedButton( - onPressed: selectedLessonPlan != null && !isEditing - ? () { - setState(() { - isEditing = true; - lessonPlanNameController.text = selectedLessonPlan!.name; - manualEntryController.text = _stripHtmlTags(selectedLessonPlan!.intro); - isSubmitDisabled = true; - }); - } - : null, - child: Text('Edit Selected'), - ), - ElevatedButton( - onPressed: isEditing - ? () async { - String manualEntryToHtml = _convertTextToHtml(manualEntryController.text); - await MoodleLmsService().updateLessonPlan( - lessonId: selectedLessonPlan!.id, - name: lessonPlanNameController.text, - intro: manualEntryToHtml, - available: 1672531200, //dummy value - deadline: 1675132800, //dummy value - ); - _fetchLessonPlans(int.parse(selectedCourse!)); - lessonPlanNameController.clear(); - manualEntryController.clear(); - - setState(() { - selectedLessonPlan = null; - isEditing = false; - isSubmitDisabled = false; - }); - } - : null, - child: Text('Save Edits'), - ), - ], - ), - ], - ), - ], - ); - }, - ), - ), - ); - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/Views/m_assessment_view.dart b/team_a/teamA/lib/Views/m_assessment_view.dart deleted file mode 100644 index a7000ef8..00000000 --- a/team_a/teamA/lib/Views/m_assessment_view.dart +++ /dev/null @@ -1,158 +0,0 @@ -import "package:flutter/material.dart"; -import "package:learninglens_app/Api/lms/factory/lms_factory.dart"; -import "package:learninglens_app/Views/view_quiz.dart"; -import 'package:learninglens_app/beans/quiz.dart'; -import 'package:learninglens_app/beans/course.dart'; -import "package:learninglens_app/Controller/custom_appbar.dart"; -import "package:learninglens_app/content_carousel.dart"; - -//The Page -class MAssessmentsView extends StatefulWidget { - MAssessmentsView({super.key, this.quizID = 0, this.courseID = 0}); - - final int quizID; - final int? courseID; - - @override - _AssessmentsState createState() => _AssessmentsState(); -} - -class _AssessmentsState extends State { - late Future?> quizzes; - Quiz? selectedQuiz; - - List> questionsData = []; - - @override - void initState() { - super.initState(); - quizzes = getAllQuizzes(widget.courseID); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: 'Assessments', - onRefresh: () { - setState(() { - quizzes = getAllQuizzes(widget.courseID); - }); - }, - userprofileurl: LmsFactory.getLmsService().profileImage ?? '', - ), - body: Column( - children: [ - Row(children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: CreateButton('assessment')) - ]), - Expanded( - child: FutureBuilder?>( - future: quizzes, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center(child: Text('Error loading quizzes')); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return Center(child: Text('No quizzes found')); - } else { - final quizList = snapshot.data!; - - return LayoutBuilder( - builder: (context, constraints) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - // Left-side course list with border - Expanded( - flex: 1, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8.0), - ), - margin: EdgeInsets.all(8.0), - child: ListView.builder( - itemCount: quizList.length, - itemBuilder: (context, index) { - final quiz = quizList[index]; - final activeCourse = - getCourse(quiz.coursedId); - if (quiz.id == widget.quizID) { - selectedQuiz = quiz; - } - - return ListTile( - title: Text( - '${quiz.name} (${activeCourse.shortName}${activeCourse.courseId})'), - subtitle: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'Due: ${quiz.timeClose == null ? "No due date set" : Course.dateFormatted(quiz.timeClose!)}'), - ], - ), - tileColor: selectedQuiz == quiz - ? Theme.of(context) - .colorScheme - .primary - .withOpacity(0.1) - : null, - onTap: () { - setState(() { - selectedQuiz = quiz; - }); - }, - ); - }, - ), - ), - ), - - // Right-side course details (quiz questions) - Expanded( - flex: 2, - child: selectedQuiz == null && widget.quizID == 0 - ? Center( - child: - Text('Select a quiz to view details')) - : ViewQuiz(quizId: selectedQuiz?.id ?? widget.quizID), - ), - ], - ); - }, - ); - } - }, - ), - ), - ], - ), - ); - } -} - -//Helper function that pulls the quizzes from all the user's courses -Future> getAllQuizzes(int? courseID) async { - List result = []; - for (Course c in LmsFactory.getLmsService().courses ?? []) { - if (courseID == 0 || courseID == null || c.id == courseID) { - result.addAll(c.quizzes ?? []); - } - } - return result; -} - -//Helper function that gets the course number for the quiz -Course getCourse(int? courseID) { - for (Course c in LmsFactory.getLmsService().courses ?? []) { - if (c.id == courseID) { - return c; - } - } - throw "No course found."; -} \ No newline at end of file diff --git a/team_a/teamA/lib/Views/quiz_generator.dart b/team_a/teamA/lib/Views/quiz_generator.dart deleted file mode 100644 index 6dbd2b58..00000000 --- a/team_a/teamA/lib/Views/quiz_generator.dart +++ /dev/null @@ -1,397 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:learninglens_app/Api/llm/prompt_engine.dart'; -import 'package:learninglens_app/Api/llm/perplexity_api.dart'; -import 'package:learninglens_app/Api/lms/constants/learning_lens.constants.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:learninglens_app/beans/assignment_form.dart'; -import 'package:learninglens_app/Controller/custom_appbar.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; -import 'edit_questions.dart'; -import 'package:learninglens_app/Api/llm/enum/llm_enum.dart'; -import 'package:learninglens_app/Api/llm/openai_api.dart'; -import 'package:learninglens_app/Api/llm/grok_api.dart'; - -class CreateAssessment extends StatefulWidget { - static TextEditingController nameController = TextEditingController(); - static TextEditingController descriptionController = TextEditingController(); - static TextEditingController subjectController = TextEditingController(); - static TextEditingController multipleChoiceController = - TextEditingController(); - static TextEditingController trueFalseController = TextEditingController(); - static TextEditingController shortAnswerController = TextEditingController(); - static TextEditingController topicController = TextEditingController(); - static TextEditingController llmController = TextEditingController(); - - CreateAssessment(); - - @override - State createState() { - return _AssessmentState(); - } -} - -class _AssessmentState extends State { - double paddingHeight = 16.0, paddingWidth = 32; - bool isAdvancedModeOnGetFromGlobalVarsLater = false; - final _formKey = GlobalKey(); - String? selectedSubject, selectedGradeLevel; - LlmType? selectedLLM; - List _subjects = [ - 'Math', - 'Science', - 'Language Arts', - 'Social Studies', - 'Health', - 'Art', - 'Music' - ]; - bool _isLoading = false; - - _AssessmentState(); - - void generateQuiz(Map fields) { - if (_formKey.currentState!.validate()) { - // Parse question counts, defaulting to 0 if empty - int multipleChoiceCount = - int.tryParse(fields['multipleChoice']!.text) ?? 0; - int trueFalseCount = int.tryParse(fields['trueFalse']!.text) ?? 0; - int shortAnswerCount = int.tryParse(fields['shortAns']!.text) ?? 0; - - // Check if at least one type of question is greater than 1 - if (multipleChoiceCount <= 0 && - trueFalseCount <= 0 && - shortAnswerCount <= 0) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Please ensure at least one type of question has a count greater than 0.'), - ), - ); - return; - } - - AssignmentForm af = AssignmentForm( - subject: selectedSubject != null - ? selectedSubject.toString() - : fields['subject']!.text, - topic: fields['description']!.text, - gradeLevel: selectedGradeLevel.toString(), - title: fields['name']!.text, - trueFalseCount: trueFalseCount, - shortAnswerCount: shortAnswerCount, - multipleChoiceCount: multipleChoiceCount, - maximumGrade: 100, - ); - generateQuestions(af); - } - } - - Future generateQuestions(AssignmentForm af) async { - try { - setState(() { - _isLoading = true; - }); - final aiModel; - if (selectedLLM == LlmType.CHATGPT) { - aiModel = OpenAiLLM(LocalStorageService.getOpenAIKey()); - } else if (selectedLLM == LlmType.GROK) { - aiModel = GrokLLM(LocalStorageService.getGrokKey()); - } else if (selectedLLM == LlmType.PERPLEXITY) { - aiModel = PerplexityLLM(LocalStorageService.getPerplexityKey()); - } else { - aiModel = OpenAiLLM(LocalStorageService.getOpenAIKey()); - } - var result = await aiModel.postToLlm(PromptEngine.generatePrompt(af)); - if (result.isNotEmpty) { - setState(() { - _isLoading = false; - }); - Navigator.push(context, - MaterialPageRoute(builder: (context) => EditQuestions(result))); - } - } catch (e) { - print("Failure sending request to LLM: $e"); - setState(() { - _isLoading = false; - }); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: 'Create Assessment', - onRefresh: () { - // _loadCourses(); - }, - userprofileurl: LmsFactory.getLmsService().profileImage ?? '', - ), - body: Form( - key: _formKey, - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - child: _isLoading - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - CircularProgressIndicator(), - SizedBox(height: paddingHeight), - Text( - 'Generating Quiz Questions...', - style: TextStyle( - fontSize: 18, color: Colors.black54), - ) - ], - ), - ) - : SingleChildScrollView( - child: Table( - columnWidths: const {0: FlexColumnWidth(2)}, - children: [ - TableRow(children: [ - SizedBox(height: paddingHeight) - ]), - TableRow(children: [ - TextEntry._('Assessment Name', true, - CreateAssessment.nameController) - ]), - TableRow(children: [ - SizedBox(height: paddingHeight) - ]), - TableRow(children: [ - TextEntry._( - 'Description', - false, - CreateAssessment.descriptionController, - isTextArea: true, - ) - ]), - TableRow(children: [ - SizedBox(height: paddingHeight) - ]), - TableRow(children: [ - isAdvancedModeOnGetFromGlobalVarsLater - ? TextEntry._('Question Subject', true, - CreateAssessment.subjectController) - : DropdownButtonFormField( - value: selectedSubject, - decoration: const InputDecoration( - labelText: "Select Subject"), - onChanged: (String? newValue) { - setState(() { - selectedSubject = newValue; - }); - }, - validator: (value) { - if (value == null) { - return 'Please select a subject.'; - } - return null; - }, - items: - _subjects.map((String value) { - return DropdownMenuItem( - value: value, - child: Text(value)); - }).toList(), - ) - ]), - TableRow(children: [ - SizedBox(height: paddingHeight) - ]), - TableRow(children: [ - DropdownButtonFormField( - value: selectedGradeLevel, - decoration: const InputDecoration( - labelText: "Select Grade Level"), - onChanged: (String? newValue) { - setState(() { - selectedGradeLevel = newValue; - }); - }, - validator: (value) { - if (value == null) { - return 'Please select a grade level.'; - } - return null; - }, - items: LearningLensConstants.gradeLevels - .map((String value) { - return DropdownMenuItem( - value: value, child: Text(value)); - }).toList(), - ) - ]), - TableRow(children: [ - SizedBox(height: paddingHeight) - ]), - TableRow(children: [ - NumberEntry._( - 'Total Multiple Choice Questions', - true, - CreateAssessment - .multipleChoiceController) - ]), - TableRow(children: [ - SizedBox(height: paddingHeight) - ]), - TableRow(children: [ - NumberEntry._( - 'Total True / False Questions', - true, - CreateAssessment.trueFalseController) - ]), - TableRow(children: [ - SizedBox(height: paddingHeight) - ]), - TableRow(children: [ - NumberEntry._( - 'Total Short Answer Questions', - true, - CreateAssessment.shortAnswerController) - ]), - ], - ), - ), - ) - ], - ), - ), - SizedBox(width: paddingWidth), - Expanded( - flex: 3, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox(height: paddingHeight), - Text( - "Choose a total number of questions equal to four or five times the number of students in the course to guarantee unique quizzes per student", - ), - SizedBox(height: paddingHeight), - DropdownButtonFormField( - value: selectedLLM, - decoration: - const InputDecoration(labelText: "Select Model"), - onChanged: (LlmType? newValue) { - setState(() { - selectedLLM = newValue; - CreateAssessment.llmController.text = - newValue!.displayName; - }); - }, - validator: (value) { - if (value == null) { - return 'Please select an LLM model to generate the quiz.'; - } - return null; - }, - items: LlmType.values.map((LlmType llm) { - return DropdownMenuItem( - value: llm, - enabled: LocalStorageService.userHasLlmKey(llm), - child: Text( - llm.displayName, - style: TextStyle( - color: LocalStorageService.userHasLlmKey(llm) - ? Colors.black87 - : Colors.grey, - ), - ), - ); - }).toList(), - ), - SizedBox(height: paddingHeight), - ElevatedButton( - onPressed: () => generateQuiz({ - "name": CreateAssessment.nameController, - "description": CreateAssessment.descriptionController, - "subject": CreateAssessment.subjectController, - "multipleChoice": - CreateAssessment.multipleChoiceController, - "trueFalse": CreateAssessment.trueFalseController, - "shortAns": CreateAssessment.shortAnswerController, - }), - child: Text("Submit"), - ) - ], - ), - ) - ], - ), - ), - ), - ); - } -} - -class NumberEntry extends StatelessWidget { - final String title; - final bool needsValidation; - final TextEditingController controller; - - NumberEntry._(this.title, this.needsValidation, this.controller); - - @override - Widget build(BuildContext context) { - return TextFormField( - controller: controller, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - validator: (value) { - if (value == null || value.isEmpty) { - controller.text = '0'; // Default to 0 if empty - return null; - } - int? numValue = int.tryParse(value); - if (numValue == null) { - return 'Please enter a valid number for $title.'; - } - return null; - }, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: title, - ), - ); - } -} - -class TextEntry extends StatelessWidget { - final String title; - final bool needsValidation, isTextArea; - final TextEditingController controller; - - TextEntry._(this.title, this.needsValidation, this.controller, - {this.isTextArea = false}); - - @override - Widget build(BuildContext context) { - return TextFormField( - controller: controller, - validator: (value) { - if (needsValidation && (value == null || value.isEmpty)) { - return 'Please enter a value for $title'; - } - return null; - }, - decoration: InputDecoration( - border: OutlineInputBorder(), - labelText: title, - ), - maxLines: isTextArea ? 6 : 1, - ); - } -} diff --git a/team_a/teamA/lib/Views/send_essay_to_moodle.dart b/team_a/teamA/lib/Views/send_essay_to_moodle.dart deleted file mode 100644 index 65d567b3..00000000 --- a/team_a/teamA/lib/Views/send_essay_to_moodle.dart +++ /dev/null @@ -1,586 +0,0 @@ -import 'package:flutter/material.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'; -import 'package:learninglens_app/Views/dashboard.dart'; -import 'dart:convert'; - -class EssayAssignmentSettings extends StatefulWidget { - final String updatedJson; - final String description; - - EssayAssignmentSettings(this.updatedJson, this.description); - - @override - EssayAssignmentSettingsState createState() => EssayAssignmentSettingsState(); -} - -class EssayAssignmentSettingsState extends State { - // Global key for the form - final _formKey = GlobalKey(); - - // Date selection variables for "Allow submissions from" - String selectedDaySubmission = '01'; - String selectedMonthSubmission = 'January'; - String selectedYearSubmission = '2025'; - String selectedHourSubmission = '00'; - String selectedMinuteSubmission = '00'; - - // Date selection variables for "Due date" - String selectedDayDue = '01'; - String selectedMonthDue = 'January'; - String selectedYearDue = '2025'; - String selectedHourDue = '00'; - String selectedMinuteDue = '00'; - - // Checkbox states - bool isSubmissionEnabled = true; - bool isDueDateEnabled = true; - - List days = - List.generate(31, (index) => (index + 1).toString().padLeft(2, '0')); - List months = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December' - ]; - List years = ['2023', '2024', '2025']; - List hours = - List.generate(24, (index) => index.toString().padLeft(2, '0')); - List minutes = - List.generate(60, (index) => index.toString().padLeft(2, '0')); - - TextEditingController _assignmentNameController = TextEditingController(); - TextEditingController _assignmentSectionController = TextEditingController(); - TextEditingController _descriptionController = TextEditingController(); - - // List of courses fetched from the controller - List courses = []; - String selectedCourse = 'Select a course'; - - @override - void initState() { - super.initState(); - fetchCourses(); // Fetch courses on page load - populateHeadersAndRows(); - _descriptionController = TextEditingController(text: widget.description); - } - - // Fetch courses from the controller - Future fetchCourses() async { - try { - List? courseList = LmsFactory.getLmsService().courses; - setState(() { - courses = courseList ?? []; - // Don't auto-select any course here, leave it to the user to select. - selectedCourse = 'Select a course'; - }); - } catch (e) { - debugPrint('Error fetching courses: $e'); - setState(() { - selectedCourse = 'No courses available'; // Handle the empty case - }); - } - } - - // Headers and Rows for Rubric Display - List headers = []; - List rows = []; - - // Function to populate headers and rows for rubric display - void populateHeadersAndRows() { - try { - Map jsonData = jsonDecode(widget.updatedJson); - - List levels = - List.from(jsonData['criteria'][0]['levels'] as List); - headers = [ - {"title": 'Criteria', 'index': 1, 'key': 'name'}, - ]; - - for (int i = 0; i < levels.length; i++) { - headers.add({ - "title": '${levels[i]['score']}', // The score (5, 3, 1) as headers - 'index': i + 2, - 'key': 'level_$i' - }); - } - - rows = (jsonData['criteria'] ?? []).map((criterion) { - Map row = { - "name": criterion['description'], - }; - - for (int i = 0; i < (criterion['levels'] as List).length; i++) { - row['level_$i'] = (criterion['levels'] as List)[i]['definition']; - } - - return row; - }).toList(); - } catch (e) { - // debugPrint('Error parsing rubric JSON: $e');// i dnt want to see this jason rubrick on console.this wass for testing only - } - - setState(() {}); - } - - // Dropdown to display courses with "Select a course" as the default option - DropdownButtonFormField _buildCourseDropdown() { - return DropdownButtonFormField( - value: selectedCourse == 'Select a course' - ? null - : selectedCourse, // Set initial value to null if 'Select a course' - decoration: InputDecoration( - labelText: 'Course name', - border: OutlineInputBorder(), - ), - validator: (value) { - if (value == null || value == 'Select a course') { - return 'Please select a course'; - } - return null; - }, - onChanged: (String? newValue) { - setState(() { - selectedCourse = newValue!; - }); - // debugPrint('Selected course: $selectedCourse');//i dnt want to see this on console.this wass for testing only - }, - items: [ - DropdownMenuItem( - value: 'Select a course', - child: Text('Select a course'), - ), - ...courses.map>((Course course) { - return DropdownMenuItem( - value: course.fullName, - child: Text(course.fullName), - ); - // }).toList(),/// trying to see if warning go away safia - }), - ], - isExpanded: true, - ); - } - - // Custom Widget to build rubric table without editable package - Widget buildRubricTable() { - return Table( - border: TableBorder.all(), - children: [ - TableRow(children: [ - _buildTableCell('Criteria'), - for (var header in headers.skip(1)) _buildTableCell(header['title']), - ]), - for (var row in rows) - TableRow(children: [ - _buildTableCell(row['name']), - for (var i = 0; i < headers.length - 1; i++) - _buildTableCell(row['level_$i']), - ]), - ], - ); - } - - Widget _buildTableCell(String text) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - text, - style: TextStyle(fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - ); - } - - // Function to validate if the availability date selections are not default values - bool _validateAvailabilityDates() { - if (selectedDaySubmission == '01' && - selectedMonthSubmission == 'January' && - selectedYearSubmission == '2025' && - selectedHourSubmission == '00' && - selectedMinuteSubmission == '00') { - return false; // If the default date for submission is selected, return false - } - - if (selectedDayDue == '01' && - selectedMonthDue == 'January' && - selectedYearDue == '2025' && - selectedHourDue == '00' && - selectedMinuteDue == '00') { - return false; // If the default due date is selected, return false - } - - return true; // If both dates have been customized, validation passes - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - double screenWidth = constraints.maxWidth; - - // Example: Calculate sizes dynamically based on screen width - double buttonWidth = - screenWidth * 0.4; // Buttons take 40% of screen width - double descriptionHeight = screenWidth * - 0.2; // Description box takes 20% of screen width height - - return Scaffold( - appBar: CustomAppBar( - title: 'Assign Essay', - userprofileurl: LmsFactory.getLmsService().profileImage ?? ''), - body: SingleChildScrollView( - padding: EdgeInsets.all(14.0), - child: Form( - key: _formKey, // Assigning the global form key - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Padding( - padding: const EdgeInsets.only(top: 14.0), - child: Text( - 'Send Essay to Moodle', - style: TextStyle( - fontSize: 18, fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - ), - ), - SizedBox(height: 20), - sectionTitle(title: 'General'), - _buildCourseDropdown(), - SizedBox(height: 12), - TextFormField( - controller: _assignmentSectionController, - decoration: InputDecoration( - labelText: 'Section Number', - border: OutlineInputBorder(), - ), - // Adding validator to ensure assignment name is not empty - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a section number'; - } - return null; - }, - ), - SizedBox(height: 12), - TextFormField( - controller: _assignmentNameController, - decoration: InputDecoration( - labelText: 'Assignment name', - border: OutlineInputBorder(), - ), - // Adding validator to ensure assignment name is not empty - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter an assignment name'; - } - return null; - }, - ), - SizedBox(height: 12), - sectionTitle(title: 'Rubric'), - buildRubricTable(), - SizedBox(height: 20), - sectionTitle(title: 'Description'), - Container( - height: descriptionHeight, // Responsive height - padding: EdgeInsets.all(8.0), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8.0), - ), - child: TextFormField( - controller: _descriptionController, - maxLines: null, // Allows multiline input - expands: true, - decoration: InputDecoration( - hintText: 'Enter description here.', - border: InputBorder.none, - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Description cannot be empty'; - } - return null; - }, - ), - ), - SizedBox(height: 20), - sectionTitle(title: 'Availability'), - SizedBox(height: 14), - Row( - children: [ - Checkbox( - value: isSubmissionEnabled, - onChanged: (value) { - setState(() { - isSubmissionEnabled = value!; - }); - }, - ), - Text('Enable'), - SizedBox(width: 10), - Expanded( - child: _buildDropdown( - 'Allow submissions from', - selectedDaySubmission, - selectedMonthSubmission, - selectedYearSubmission, - selectedHourSubmission, - selectedMinuteSubmission, - isSubmissionEnabled, - (String? newValue) { - setState(() { - selectedDaySubmission = newValue!; - }); - }, - (String? newValue) { - setState(() { - selectedMonthSubmission = newValue!; - }); - }, - (String? newValue) { - setState(() { - selectedYearSubmission = newValue!; - }); - }, - (String? newValue) { - setState(() { - selectedHourSubmission = newValue!; - }); - }, - (String? newValue) { - setState(() { - selectedMinuteSubmission = newValue!; - }); - }, - ), - ), - ], - ), - SizedBox(height: 14), - Row( - children: [ - Checkbox( - value: isDueDateEnabled, - onChanged: (value) { - setState(() { - isDueDateEnabled = value!; - }); - }, - ), - Text('Enable'), - SizedBox(width: 10), - Expanded( - child: _buildDropdown( - 'Due date', - selectedDayDue, - selectedMonthDue, - selectedYearDue, - selectedHourDue, - selectedMinuteDue, - isDueDateEnabled, - (String? newValue) { - setState(() { - selectedDayDue = newValue!; - }); - }, - (String? newValue) { - setState(() { - selectedMonthDue = newValue!; - }); - }, - (String? newValue) { - setState(() { - selectedYearDue = newValue!; - }); - }, - (String? newValue) { - setState(() { - selectedHourDue = newValue!; - }); - }, - (String? newValue) { - setState(() { - selectedMinuteDue = newValue!; - }); - }, - ), - ), - ], - ), - SizedBox(height: 14), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - SizedBox( - width: buttonWidth, // Responsive button width - child: ElevatedButton( - onPressed: () async { - // Validate the form before submitting - if (_formKey.currentState!.validate() && - _descriptionController.text.trim().isNotEmpty && - _validateAvailabilityDates()) { - var api = LmsFactory.getLmsService(); - bool? token = api.isLoggedIn(); - - if (token) { - String courseId = courses - .firstWhere((course) => - course.fullName == selectedCourse) - .id - .toString(); - String assignmentName = - _assignmentNameController.text; - String sectionNumber = - _assignmentSectionController.text; - String description = _descriptionController.text.trim(); - String dueDate = - '$selectedDayDue $selectedMonthDue $selectedYearDue $selectedHourDue:$selectedMinuteDue'; - String allowSubmissionFrom = - '$selectedDaySubmission $selectedMonthSubmission $selectedYearSubmission $selectedHourSubmission:$selectedMinuteSubmission'; - - await api.createAssignment( - courseId, - sectionNumber, // Section ID - assignmentName, - allowSubmissionFrom, - dueDate, - widget.updatedJson, - description, - ); - - - if (mounted) { - - final snackBar = SnackBar( - content: Text('Assignment submitted successfully!'), - duration: Duration(seconds: 2), - ); - ScaffoldMessenger.of(context).showSnackBar(snackBar); - await Future.delayed(snackBar.duration); - if (mounted) { - Navigator.pop(context); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => TeacherDashboard(), - ), - ); - } - - } - - - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Failed to retrieve user token.'))); - } - } else { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - 'Please fill out all fields and ensure a course, description, and valid availability dates are selected.'))); - } - }, - child: Text('Send to Moodle'), - ), - ), - SizedBox( - width: buttonWidth, // Responsive button width - child: ElevatedButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text('Go Back to Edit Essay'), - ), - ), - ], - ), - ], - ), - ), - ), - ); - }, - ); - } - - // Dropdown Builder for each section - Widget _buildDropdown( - String label, - String selectedDay, - String selectedMonth, - String selectedYear, - String selectedHour, - String selectedMinute, - bool isEnabled, - ValueChanged onDayChanged, - ValueChanged onMonthChanged, - ValueChanged onYearChanged, - ValueChanged onHourChanged, - ValueChanged onMinuteChanged) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label), - Wrap( - spacing: 10.0, // Space between dropdowns - runSpacing: 8.0, // Space between rows when wrapping - children: [ - _buildDropdownButton(days, selectedDay, onDayChanged, isEnabled), - _buildDropdownButton( - months, selectedMonth, onMonthChanged, isEnabled), - _buildDropdownButton(years, selectedYear, onYearChanged, isEnabled), - _buildDropdownButton(hours, selectedHour, onHourChanged, isEnabled), - _buildDropdownButton( - minutes, selectedMinute, onMinuteChanged, isEnabled), - ], - ), - ], - ); - } - - // Dropdown Button Builder - Widget _buildDropdownButton(List items, String selectedValue, - ValueChanged onChanged, bool isEnabled) { - return SizedBox( - width: 103, // Adjust width as necessary - child: DropdownButton( - value: selectedValue, - onChanged: isEnabled ? onChanged : null, - items: items.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - ), - ); - } - - // Section Title Widget - Widget sectionTitle({required String title}) { - return Text( - title, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ); - } -} diff --git a/team_a/teamA/lib/Views/send_quiz_to_moodle.dart b/team_a/teamA/lib/Views/send_quiz_to_moodle.dart deleted file mode 100644 index b9c0117d..00000000 --- a/team_a/teamA/lib/Views/send_quiz_to_moodle.dart +++ /dev/null @@ -1,632 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:learninglens_app/Api/lms/enum/lms_enum.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:learninglens_app/Api/lms/google_classroom/google_lms_service.dart'; -import 'package:learninglens_app/beans/quiz.dart'; -import 'package:learninglens_app/beans/course.dart'; -import 'package:learninglens_app/Controller/custom_appbar.dart'; -import 'package:learninglens_app/Views/dashboard.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; -import '../Api/lms/moodle/moodle_lms_service.dart'; // Make sure this path is correct - -class QuizMoodle extends StatefulWidget { - final Quiz quiz; - QuizMoodle({required this.quiz}); - - @override - QuizMoodleState createState() => QuizMoodleState(); -} - -class QuizMoodleState extends State { - // Submission form dates - String selectedDaySubmission = '01'; - String selectedMonthSubmission = 'January'; - String selectedYearSubmission = '2025'; // Start from 2025 - String selectedHourSubmission = '00'; - String selectedMinuteSubmission = '00'; - late String quizasxml; - late MoodleLmsService api; - List courses = []; - String selectedCourse = 'Select a course'; - - LmsType? lmsType; - bool isLoading = false; // Added to track loading state - - @override - void initState() { - super.initState(); - quizNameController = TextEditingController(text: widget.quiz.name ?? ''); - quizQuestionsController = TextEditingController(); - quizasxml = widget.quiz.toXmlString(); - fetchCourses(); - _getLmsType(); // Fetch LMS type on init - } - - // Fetch LMS type from local storage - Future _getLmsType() async { - LmsType retrievedLmsType = LocalStorageService.getSelectedClassroom(); - setState(() { - lmsType = retrievedLmsType; - }); - } - - // Fetch courses from the controller - Future fetchCourses() async { - try { - List? courseList = LmsFactory.getLmsService().courses; - setState(() { - courses = courseList ?? []; - selectedCourse = 'Select a course'; - }); - } catch (e) { - debugPrint('Error fetching courses: $e'); - setState(() { - selectedCourse = 'No courses available'; // Handle the empty case - }); - } - } - - // Dropdown to display courses with "Select a course" as the default option - DropdownButtonFormField _buildCourseDropdown() { - return DropdownButtonFormField( - value: selectedCourse == 'Select a course' ? null : selectedCourse, - decoration: InputDecoration( - labelText: 'Course name', - border: OutlineInputBorder(), - ), - validator: (value) { - if (value == null || value == 'Select a course') { - return 'Please select a course'; - } - return null; - }, - onChanged: (String? newValue) { - setState(() { - selectedCourse = newValue!; - }); - debugPrint('Selected course: $selectedCourse'); - }, - items: [ - DropdownMenuItem( - value: 'Select a course', - child: Text('Select a course'), - ), - ...courses.map>((Course course) { - return DropdownMenuItem( - value: course.id.toString(), - child: Text(course.fullName), - ); - }), - ], - isExpanded: true, - ); - } - - // Due date selection - String selectedDayDue = '01'; - String selectedMonthDue = 'January'; - String selectedYearDue = '2025'; // Start from 2025 - String selectedHourDue = '00'; - String selectedMinuteDue = '00'; - - // Checkbox states - bool isSubmissionEnabled = true; - bool isDueDateEnabled = true; - - List days = - List.generate(31, (index) => (index + 1).toString().padLeft(2, '0')); - List months = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December' - ]; - List years = ['2025', '2026', '2027']; // Years starting from 2025 - List hours = - List.generate(24, (index) => index.toString().padLeft(2, '0')); - List minutes = - List.generate(60, (index) => index.toString().padLeft(2, '0')); - - // Initial selected values for dropdowns - String selectedCategory = 'No Category Selected'; - String selectedAttempt = 'No attempt limit'; - String selectedGradingMethod = 'No grading method selected'; - - // Lists of static items for dropdowns - var categoryItems = [ - 'No Category Selected', - 'Category 1', - 'Category 2', - 'Category 3' - ]; - var attemptItems = [ - 'No attempt limit', - 'Unlimited', - 'First', - 'Second', - 'Last' - ]; - var gradingMethodItems = [ - 'No grading method selected', - 'Highest Grade', - 'Average Grade', - 'Low Grade' - ]; - - late TextEditingController quizNameController; - TextEditingController gradeController = TextEditingController(); - late TextEditingController quizQuestionsController; - TextEditingController quizSectionController = TextEditingController(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - title: 'Assign Assessment', - userprofileurl: LmsFactory.getLmsService().profileImage ?? ''), - body: SingleChildScrollView( - padding: EdgeInsets.all(15.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Padding( - padding: const EdgeInsets.only(top: 15.0), - child: Text( - 'Assign Quiz', - textDirection: TextDirection.ltr, - style: TextStyle(fontSize: 25, fontWeight: FontWeight.normal), - textAlign: TextAlign.center, - ), - ), - ), - SizedBox(height: 30), - sectionTitle(title: 'Course Name'), - _buildCourseDropdown(), - SizedBox(height: 15), - sectionTitle(title: 'Quiz Name'), - SizedBox(height: 15), - TextField( - controller: quizNameController, - decoration: InputDecoration( - labelText: 'Quiz name', - border: OutlineInputBorder(), - ), - enabled: false, - ), - SizedBox(height: 15), - sectionTitle(title: 'Section Number'), - SizedBox(height: 15), - TextField( - controller: quizSectionController, - decoration: InputDecoration( - labelText: 'Course Section Number', - border: OutlineInputBorder(), - ), - keyboardType: TextInputType.number, // Set keyboard type to number - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly // Allow only digits - ], - ), - SizedBox(height: 15), - sectionTitle(title: 'Number of Questions'), - SizedBox(height: 15), - TextField( - controller: quizQuestionsController, - decoration: InputDecoration( - labelText: 'Number of questions', - border: OutlineInputBorder(), - ), - ), - SizedBox(height: 15), - sectionTitle(title: 'Availability'), - SizedBox(height: 15), - // Submission Date - Directionality( - textDirection: TextDirection.ltr, - child: Row( - children: [ - Checkbox( - value: isSubmissionEnabled, - onChanged: (value) { - setState(() { - isSubmissionEnabled = value!; - }); - }), - Text('Enable'), - SizedBox(width: 10), - _buildDropdown( - 'Allow Submissions From Date:', - selectedDaySubmission, - selectedMonthSubmission, - selectedYearSubmission, - selectedHourSubmission, - selectedMinuteSubmission, - isSubmissionEnabled, (String? newValue) { - setState(() { - selectedDaySubmission = newValue!; - }); - }, (String? newValue) { - setState(() { - selectedMonthSubmission = newValue!; - }); - }, (String? newValue) { - setState(() { - selectedYearSubmission = newValue!; - }); - }, (String? newValue) { - setState(() { - selectedHourSubmission = newValue!; - }); - }, (String? newValue) { - setState(() { - selectedMinuteSubmission = newValue!; - }); - }), - ], - ), - ), - SizedBox(height: 16), - // Due Date - Directionality( - textDirection: TextDirection.ltr, - child: Row( - children: [ - Checkbox( - value: isDueDateEnabled, - onChanged: (value) { - setState(() { - isDueDateEnabled = value!; - }); - }, - ), - Text('Enable'), - SizedBox(width: 10), - _buildDropdown( - 'Due Date:', - selectedDayDue, - selectedMonthDue, - selectedYearDue, - selectedHourDue, - selectedMinuteDue, - isDueDateEnabled, (String? newValue) { - setState(() { - selectedDayDue = newValue!; - }); - }, (String? newValue) { - setState(() { - selectedMonthDue = newValue!; - }); - }, (String? newValue) { - setState(() { - selectedYearDue = newValue!; - }); - }, (String? newValue) { - setState(() { - selectedHourDue = newValue!; - }); - }, (String? newValue) { - setState(() { - selectedMinuteDue = newValue!; - }); - }), - ], - ), - ), - SizedBox(height: 16), - Directionality( - textDirection: TextDirection.ltr, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (lmsType == LmsType.MOODLE) - ElevatedButton( - onPressed: isLoading - ? null - : () async { - setState(() { - isLoading = true; - }); - - if (selectedCourse == 'Select a course') { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Please select a course to proceed.')), - ); - setState(() { - isLoading = false; - }); - return; - } - - try { - DateTime submissionDate = DateTime( - int.parse(selectedYearSubmission), - months.indexOf(selectedMonthSubmission) + 1, - int.parse(selectedDaySubmission), - int.parse(selectedHourSubmission), - int.parse(selectedMinuteSubmission), - ); - - DateTime dueDate = DateTime( - int.parse(selectedYearDue), - months.indexOf(selectedMonthDue) + 1, - int.parse(selectedDayDue), - int.parse(selectedHourDue), - int.parse(selectedMinuteDue), - ); - - String formattedDueDate = - "${dueDate.year}-${dueDate.month.toString().padLeft(2, '0')}-${dueDate.day.toString().padLeft(2, '0')}-${dueDate.hour.toString().padLeft(2, '0')}-${dueDate.minute.toString().padLeft(2, '0')}"; - - var quizid = await LmsFactory.getLmsService() - .createQuiz( - selectedCourse, - widget.quiz.name ?? 'Quiz Name', - widget.quiz.description ?? 'Quiz Description', - quizSectionController.text, - '$selectedDaySubmission $selectedMonthSubmission $selectedYearSubmission $selectedHourSubmission:$selectedMinuteSubmission', - '$selectedDayDue $selectedMonthDue $selectedYearDue $selectedHourDue:$selectedMinuteDue', - ); - print('Quiz ID: $quizid'); - - var categoryid = await LmsFactory.getLmsService() - .importQuizQuestions( - selectedCourse, quizasxml); - print('Category ID: $categoryid'); - - var randomresult = await LmsFactory.getLmsService() - .addRandomQuestions( - categoryid.toString(), - quizid.toString(), - quizQuestionsController.text); - print('Random Result: $randomresult'); - - if (randomresult == 'true') { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Quiz submitted successfully to Moodle!')), - ); - await Future.delayed(Duration(seconds: 2)); - if (mounted) { - Navigator.pop(context); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => TeacherDashboard(), - ), - ); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Failed to update grades in Moodle.')), - ); - } - } catch (e) { - print('Error during quiz creation: $e'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('An error occurred: ${e}')), - ); - } finally { - setState(() { - isLoading = false; - }); - } - }, - child: isLoading - ? CircularProgressIndicator( - color: Colors.white, - ) - : Text( - 'Send to Moodle', - textDirection: TextDirection.ltr, - ), - ), - if (lmsType == LmsType.GOOGLE) - ElevatedButton( - onPressed: isLoading - ? null - : () async { - setState(() { - isLoading = true; - }); - - if (selectedCourse == 'Select a course') { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Please select a course to proceed.')), - ); - setState(() { - isLoading = false; - }); - return; - } - - try { - DateTime submissionDate = DateTime( - int.parse(selectedYearSubmission), - months.indexOf(selectedMonthSubmission) + 1, - int.parse(selectedDaySubmission), - int.parse(selectedHourSubmission), - int.parse(selectedMinuteSubmission), - ); - - DateTime dueDate = DateTime( - int.parse(selectedYearDue), - months.indexOf(selectedMonthDue) + 1, - int.parse(selectedDayDue), - int.parse(selectedHourDue), - int.parse(selectedMinuteDue), - ); - - String formattedDueDate = - "${dueDate.year}-${dueDate.month.toString().padLeft(2, '0')}-${dueDate.day.toString().padLeft(2, '0')}-${dueDate.hour.toString().padLeft(2, '0')}-${dueDate.minute.toString().padLeft(2, '0')}"; - - bool success = await GoogleLmsService() - .createAndAssignQuizFromXml( - selectedCourse, - quizNameController.text, - widget.quiz.description ?? 'Quiz Description', - quizasxml, - formattedDueDate, - ); - if (success) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Quiz submitted successfully to Google Classroom!')), - ); - await Future.delayed(Duration(seconds: 2)); - if (mounted) { - Navigator.pop(context); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => TeacherDashboard(), - ), - ); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Failed to create quiz in Google Classroom.')), - ); - } - } catch (e) { - print('Error during quiz creation: $e'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('An error occurred: ${e}')), - ); - } finally { - setState(() { - isLoading = false; - }); - } - }, - child: isLoading - ? CircularProgressIndicator( - color: Colors.white, - ) - : Text( - 'Send to Google Classroom', - textDirection: TextDirection.ltr, - ), - ), - ], - ), - ), - ], - ), - ), - ); - } - - Widget _buildDropdown( - String label, - String selectedDay, - String selectedMonth, - String selectedYear, - String selectedHour, - String selectedMinute, - bool isEnabled, - ValueChanged onDayChanged, - ValueChanged onMonthChanged, - ValueChanged onYearChanged, - ValueChanged onHourChanged, - ValueChanged onMinuteChanged) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label), - Row( - children: [ - DropdownButton( - value: selectedDay, - onChanged: isEnabled ? onDayChanged : null, - items: days.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - ), - SizedBox(width: 5), - DropdownButton( - value: selectedMonth, - onChanged: isEnabled ? onMonthChanged : null, - items: months.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - ), - SizedBox(width: 5), - DropdownButton( - value: selectedYear, - onChanged: isEnabled ? onYearChanged : null, - items: years.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - ), - SizedBox(width: 5), - DropdownButton( - value: selectedHour, - onChanged: isEnabled ? onHourChanged : null, - items: hours.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - ), - SizedBox(width: 5), - DropdownButton( - value: selectedMinute, - onChanged: isEnabled ? onMinuteChanged : null, - items: minutes.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - ), - ], - ), - ], - ); - } - - Widget sectionTitle({required String title}) { - return Padding( - padding: EdgeInsets.only(bottom: 8.0), - child: Text( - title, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ); - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/Views/template_view.dart b/team_a/teamA/lib/Views/template_view.dart deleted file mode 100644 index 139623e0..00000000 --- a/team_a/teamA/lib/Views/template_view.dart +++ /dev/null @@ -1,59 +0,0 @@ -import "package:flutter/material.dart"; -import "package:learninglens_app/Api/lms/factory/lms_factory.dart"; -import "package:learninglens_app/Controller/custom_appbar.dart"; - -/// -/// Template for views -/// Need to change the class name to the name based on the view you -/// are creating to include the constructor as well. -/// Need to change the class that extends State as well. Make sure -/// that the createState function returns the same name. -/// -/// The CustomAppBar is already included, but you will need to update -/// the title string. -/// -/// The content for the page begins in the children array. There is -/// a single text box as a place holder. -/// -/// To add your view to the rest of the app, you will have to add it -/// to the dashboard.dart file. On line ~213, there is a List called -/// buttonData. You will need to update the 'onPressed' section with -/// your new template. You can see examples that Derek has already -/// done on other buttons. -/// -class TemplateView extends StatefulWidget{ - TemplateView(); - - @override - State createState(){ - return _TemplateState(); - } -} - -class _TemplateState extends State{ - - @override - Widget build(BuildContext context){ - return Scaffold( - appBar: CustomAppBar( - title: 'Template View', - onRefresh: () { - // Add refresh logic here - }, - userprofileurl: LmsFactory.getLmsService().profileImage ?? '' - ), - body: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children:[ - // Add Content here - Text( - 'Place content here', - style: TextStyle(fontSize: 20), - ), - ] - ) - ) - ); - } -} diff --git a/team_a/teamA/lib/Views/user_settings.dart b/team_a/teamA/lib/Views/user_settings.dart deleted file mode 100644 index f781d7da..00000000 --- a/team_a/teamA/lib/Views/user_settings.dart +++ /dev/null @@ -1,332 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_colorpicker/flutter_colorpicker.dart'; -import 'package:learninglens_app/Api/lms/moodle/moodle_lms_service.dart'; -import 'package:learninglens_app/Controller/custom_appbar.dart'; -import 'package:learninglens_app/notifiers/login_notifier.dart'; -import 'package:learninglens_app/notifiers/theme_notifier.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; - -class UserSettings extends StatefulWidget { - @override - UserSettingsState createState() => UserSettingsState(); -} - -class UserSettingsState extends State { - final TextEditingController _usernameController = TextEditingController(); - final TextEditingController _passwordController = TextEditingController(); - final TextEditingController _moodleUrlController = TextEditingController(); - - final TextEditingController _apiKeyController = TextEditingController(); - final TextEditingController _grokKeyController = TextEditingController(); - final TextEditingController _preplexityKeyController = TextEditingController(); - - final TextEditingController _googleClientIdController = - TextEditingController(); - - @override - void initState() { - super.initState(); - _loadStoredValues(); - } - - Future _loadStoredValues() async { - final username = LocalStorageService.getUsername(); - final password = LocalStorageService.getPassword(); - final moodleUrl = LocalStorageService.getMoodleUrl(); - final apiKey = LocalStorageService.getOpenAIKey(); - final preplexityKey = LocalStorageService.getPerplexityKey(); - final grokKey = LocalStorageService.getGrokKey(); - final googleClientId = LocalStorageService.getGoogleClientId(); - - setState(() { - _usernameController.text = username; - _passwordController.text = password; - _moodleUrlController.text = moodleUrl; - _apiKeyController.text = apiKey; - _preplexityKeyController.text = preplexityKey; - _grokKeyController.text = grokKey; - _googleClientIdController.text = googleClientId; - }); - } - - @override - Widget build(BuildContext context) { - final loginNotifier = Provider.of(context); - final themeColor = Provider.of(context).primaryColor; - - return Scaffold( - appBar: CustomAppBar( - title: 'User Settings', - onRefresh: () { - // Add refresh logic here - }, - userprofileurl: MoodleLmsService().profileImage ?? '', - ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: SingleChildScrollView( - child: Column( - children: [ - // Moodle Login Block - _buildMoodleLoginBlock(loginNotifier), - - const SizedBox(height: 20), - const Divider(), - - // Google Classroom Login Block - _buildGoogleClassroomLoginBlock(loginNotifier), - - const SizedBox(height: 20), - const Divider(), - - // API Key Block - _buildApiKeyBlock(loginNotifier), - - const SizedBox(height: 20), - const Divider(), - - // Theme Color Picker - Text( - 'Theme Color Picker:', - style: TextStyle(fontSize: 20), - ), - ElevatedButton( - onPressed: _pickColor, - style: ElevatedButton.styleFrom( - backgroundColor: themeColor, - foregroundColor: Theme.of(context).colorScheme.onPrimary, - ), - child: Text('Pick Theme Color'), - ), - ], - ), - ), - ), - ); - } - - // ------------------------------------------- - // Moodle Login Block - // ------------------------------------------- - Widget _buildMoodleLoginBlock(LoginNotifier loginNotifier) { - final moodleState = loginNotifier.moodleState; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Moodle Login:', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - TextField( - controller: _usernameController, - decoration: InputDecoration(labelText: 'Username'), - enabled: !moodleState.isLoggedIn, - ), - TextField( - controller: _passwordController, - decoration: InputDecoration(labelText: 'Password'), - obscureText: true, - enabled: !moodleState.isLoggedIn, - ), - TextField( - controller: _moodleUrlController, - decoration: InputDecoration(labelText: 'Moodle URL'), - enabled: !moodleState.isLoggedIn, - ), - const SizedBox(height: 10), - if (!moodleState.isLoggedIn) - ElevatedButton( - onPressed: () { - loginNotifier.signInWithMoodle( - _usernameController.text, - _passwordController.text, - _moodleUrlController.text, - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - ), - child: Text('Login to Moodle'), - ), - if (moodleState.isLoggedIn) - ElevatedButton( - onPressed: loginNotifier.signOutFromMoodle, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - child: Text('Logout from Moodle'), - ), - if (moodleState.errorMessage?.isNotEmpty ?? false) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - moodleState.errorMessage!, - style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), - ), - ), - ], - ); - } - - // ------------------------------------------- - // Google Classroom Login Block - // ------------------------------------------- - Widget _buildGoogleClassroomLoginBlock(LoginNotifier loginNotifier) { - final googleState = loginNotifier.googleState; // convenience variable - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Google Classroom Login:', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - TextField( - controller: _googleClientIdController, - decoration: InputDecoration(labelText: 'Client ID'), - enabled: false, // Make it non-editable - ), - const SizedBox(height: 10), - if (!googleState.isLoggedIn) - ElevatedButton( - onPressed: loginNotifier.signInWithGoogle, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - ), - child: Text('Login to Google Classroom'), - ), - if (googleState.isLoggedIn) - ElevatedButton( - onPressed: loginNotifier.signOutFromGoogle, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - foregroundColor: Colors.white, - ), - child: Text('Logout from Google Classroom'), - ), - if (googleState.errorMessage?.isNotEmpty ?? false) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - googleState.errorMessage!, - style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), - ), - ), - ], - ); - } - - // ------------------------------------------- - // API Key Block - // ------------------------------------------- - Widget _buildApiKeyBlock(LoginNotifier loginNotifier) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'API Keys:', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - _buildApiKeyField( - label: 'OpenAI API Key', - controller: _apiKeyController, - loginNotifier: loginNotifier, - keyType: LLMKey.openAI, - ), - _buildApiKeyField( - label: 'Preplexity AI API Key', - controller: _preplexityKeyController, - loginNotifier: loginNotifier, - keyType: LLMKey.perplexity, - ), - _buildApiKeyField( - label: 'Grok AI API Key', - controller: _grokKeyController, - loginNotifier: loginNotifier, - keyType: LLMKey.grok, - ), - ], - ); - } - - Widget _buildApiKeyField({ - required String label, - required TextEditingController controller, - required LoginNotifier loginNotifier, - required LLMKey keyType, - }) { - return Column( - children: [ - TextField( - controller: controller, - obscureText: true, - decoration: InputDecoration(labelText: label), - enabled: controller.text.isEmpty, - // If you want to disable the TextField once it has a value, - // keep this. Otherwise, feel free to remove "enabled: controller.text.isEmpty". - ), - const SizedBox(height: 10), - ElevatedButton( - onPressed: () { - loginNotifier.saveLLMKey(keyType, controller.text); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - ), - child: Text('Save'), - ), - const Divider(), - ], - ); - } - - // ------------------------------------------- - // Theme Color Picker - // ------------------------------------------- - void _pickColor() async { - await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text( - 'Pick a theme color', - style: TextStyle(color: Theme.of(context).colorScheme.inversePrimary), - ), - content: SingleChildScrollView( - child: SizedBox( - width: 300, - height: 50, - child: BlockPicker( - pickerColor: - Provider.of(context, listen: false).primaryColor, - onColorChanged: (color) { - Provider.of(context, listen: false) - .updateTheme(color); - }, - availableColors: [ - Colors.red, - Colors.green, - Colors.blue, - Colors.orange, - Colors.purple, - ], - ), - ), - ), - actions: [ - TextButton( - child: Text('Select'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ), - ); - } -} diff --git a/team_a/teamA/lib/Views/view_quiz.dart b/team_a/teamA/lib/Views/view_quiz.dart deleted file mode 100644 index 4d23d49e..00000000 --- a/team_a/teamA/lib/Views/view_quiz.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:learninglens_app/Controller/custom_appbar.dart'; -import 'package:learninglens_app/beans/quiz_type.dart'; - -class ViewQuiz extends StatelessWidget { - final int quizId; - final bool showAppBar = false; - - ViewQuiz({required this.quizId}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: showAppBar - ? CustomAppBar( - title: 'Assessments', - userprofileurl: LmsFactory.getLmsService().profileImage ?? '', - ) - : null, - body: SingleChildScrollView( - child: Center( - child: Column( - children: [ - Text('Questions', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), - FutureBuilder?>( - future: LmsFactory.getLmsService().getQuestionsFromQuiz(quizId), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center(child: Text('Error loading questions')); - } else if (!snapshot.hasData || snapshot.data!.isEmpty) { - return Center(child: Text('No questions found')); - } else { - final questionList = snapshot.data!; - List> questionsData = []; - questionsData = questionList.map((question) { - return { - 'questionNumber': question.name, - 'questionType': question.questionType, - 'questionText': question.questionText, - }; - }).toList(); - - return SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8.0), - ), - margin: EdgeInsets.all(8.0), - child: DataTable( - headingRowColor: MaterialStateProperty.all( - Theme.of(context) - .colorScheme - .primary - .withOpacity(0.1)), - columns: const [ - DataColumn(label: Text('Question No.')), - DataColumn(label: Text('Type')), - DataColumn(label: Text('Question Text')), - ], - rows: questionsData.map((row) { - return DataRow(cells: [ - DataCell( - SizedBox( - width: 90, - child: Text( - row['questionNumber'].toString(), - softWrap: true, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - ), - DataCell( - SizedBox( - width: 90, - child: Text( - row['questionType'].toString(), - softWrap: true, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ), - ), - DataCell( - SizedBox( - child: Text( - row['questionText'].toString(), - softWrap: true, - overflow: TextOverflow.ellipsis, - maxLines: 4, - ), - ), - ), - ]); - }).toList(), - ), - ), - ); - } - }, - ), - ], - ), - )), - ); - } -} diff --git a/team_a/teamA/lib/Views/view_submission_detail.dart b/team_a/teamA/lib/Views/view_submission_detail.dart deleted file mode 100644 index 4bc3994f..00000000 --- a/team_a/teamA/lib/Views/view_submission_detail.dart +++ /dev/null @@ -1,377 +0,0 @@ -import 'dart:convert'; -import 'package:flutter/material.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:learninglens_app/Controller/custom_appbar.dart'; -import 'package:learninglens_app/beans/participant.dart'; -import 'package:learninglens_app/beans/submission.dart'; -import 'package:learninglens_app/beans/moodle_rubric.dart'; -import 'package:learninglens_app/Views/essays_view.dart'; -import 'dart:math'; - -class SubmissionDetail extends StatefulWidget { - final Participant participant; - final Submission submission; - final String courseId; - - SubmissionDetail( - {required this.participant, - required this.submission, - required this.courseId}); - - @override - SubmissionDetailState createState() => SubmissionDetailState(); -} - -class SubmissionDetailState extends State { - MoodleRubric? rubric; - List? scores; - bool isLoading = true; - String errorMessage = ''; - Map selectedLevels = {}; // Map to store selected levels - Map remarks = {}; // Map to store remarks - Map remarkControllers = - {}; // Controllers for each remark - - @override - void initState() { - super.initState(); - fetchRubric(); - } - - Future fetchRubric() async { - print('Fetching Rubric for assignment ID: ${widget.submission.assignmentId}'); - int? contextId = await LmsFactory.getLmsService() - .getContextId(widget.submission.assignmentId, widget.courseId); - if (contextId != null) { - var fetchedRubric = await LmsFactory.getLmsService() - .getRubric(widget.submission.assignmentId.toString()); - print('Fetched Rubric: $fetchedRubric'); - var submissionScores = await LmsFactory.getLmsService().getRubricGrades( - widget.submission.assignmentId, widget.participant.id); - print('Submission Scores: $submissionScores'); - - setState(() { - rubric = fetchedRubric; - scores = submissionScores; - // Populate selectedLevels and remarks from submissionScores - for (var score in scores!) { - selectedLevels[score['criterionid']] = score['levelid']; - remarks[score['criterionid']] = score['remark'] ?? ''; - remarkControllers[score['criterionid']] = - TextEditingController(text: remarks[score['criterionid']]); - } - isLoading = false; - }); - - if (fetchedRubric == null) { - setState(() { - errorMessage = 'No rubric available for this assignment.'; - }); - } - } else { - setState(() { - isLoading = false; - errorMessage = 'Failed to retrieve context ID for the assignment.'; - }); - } - } - - // Save updated submission scores and remarks as JSON - void saveSubmissionScores() async { - List> updatedScores = []; - selectedLevels.forEach((criterionid, levelid) { - updatedScores.add({ - 'criterionid': criterionid, - 'levelid': levelid, - 'remark': remarks[criterionid] ?? '' - }); - }); - - String jsonScores = jsonEncode(updatedScores); - print('Updated Submission Scores and Remarks: $jsonScores'); - // Handle further actions like saving to a database or API here. - // SubmissionListState? submissionListState = context.findAncestorStateOfType(); - bool results = await LmsFactory.getLmsService().setRubricGrades( - widget.submission.assignmentId, widget.participant.id, jsonScores); - print('Results: $results'); - if (mounted) { - if (results) { - final snackBar = SnackBar( - content: Text('Grades updated successfully!'), - duration: Duration(seconds: 2), - ); - ScaffoldMessenger.of(context).showSnackBar(snackBar); - await Future.delayed(snackBar.duration); - if (mounted) { - Navigator.pop(context); - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => EssaysView(), - ), - ); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to update grades.')), - ); - } - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar(title: 'Submission Details From here', userprofileurl: LmsFactory.getLmsService().profileImage ?? ''), - body: isLoading - ? Center(child: CircularProgressIndicator()) - : Padding( - padding: EdgeInsets.all(16.0), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // User ID - Text( - widget.participant.fullname, - style: - TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - SizedBox(height: 8), - - // Status - Text( - 'Status: ${widget.submission.status}', - style: TextStyle(fontSize: 16), - ), - SizedBox(height: 8), - - // Submission Time - Text( - 'Submitted on: ${widget.submission.submissionTime.toLocal()}', - style: TextStyle(fontSize: 16), - ), - SizedBox(height: 16), - - // Online Text - Text( - 'Online Text:', - style: - TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - SizedBox(height: 8), - widget.submission.onlineText.isNotEmpty - ? Text( - widget.submission.onlineText.replaceAll(RegExp(r"<[^>]*>"), ""), - style: TextStyle(fontSize: 16), - ) - : Text( - 'No content provided.', - style: TextStyle( - fontSize: 16, fontStyle: FontStyle.italic), - ), - SizedBox(height: 16), - - // Rubric Section - rubric != null - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Rubric:', - style: TextStyle( - fontSize: 18, fontWeight: FontWeight.bold), - ), - SizedBox(height: 8), - - // Rubric table (replace rubricTable with new table) - buildInteractiveRubricTable(), - SizedBox(height: 16), - ElevatedButton( - onPressed: saveSubmissionScores, - child: Text('Save'), - ), - ], - ) - : errorMessage.isNotEmpty - ? Text( - errorMessage, - style: TextStyle( - fontSize: 50, - fontStyle: FontStyle.italic, - color: Colors.red), - ) - : Text( - 'No rubric available.', - style: TextStyle( - fontSize: 16, fontStyle: FontStyle.italic), - ), - ], - ), - ), - ), - ); - } - -// Interactive rubric table with dynamic width expansion - Widget buildInteractiveRubricTable() { - if (rubric == null) - return Container(); // No rubric, return an empty container - - List tableRows = []; - - // First row: Header row with scores and remarks - tableRows.add( - TableRow( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - ), - children: [ - TableCell( - child: Padding( - padding: EdgeInsets.all(8.0), - child: Text( - 'Criteria', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - softWrap: true, - overflow: TextOverflow.visible, - ), - ), - ), - ...rubric!.criteria.first.levels.map((level) { - return TableCell( - child: Padding( - padding: EdgeInsets.all(8.0), - child: Align( - alignment: Alignment.center, - child: Text( - '${level.score} pts', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - softWrap: true, - overflow: TextOverflow.visible, - ), - ), - ), - ); - }).toList(), - TableCell( - child: Padding( - padding: EdgeInsets.all(8.0), - child: Text( - 'Remarks', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - softWrap: true, - overflow: TextOverflow.visible, - ), - ), - ), - ], - ), - ); - - // Add rows for each criterion - for (var criterion in rubric!.criteria) { - tableRows.add( - TableRow( - children: [ - TableCell( - child: Padding( - padding: EdgeInsets.all(8.0), - child: Text( - criterion.description, - softWrap: true, - overflow: TextOverflow.visible, - ), - ), - ), - ...criterion.levels.map((level) { - bool isSelected = selectedLevels[criterion.id] == level.id; - return TableCell( - verticalAlignment: TableCellVerticalAlignment.fill, - child: InkWell( - onTap: () { - setState(() { - selectedLevels[criterion.id] = level.id; - }); - }, - child: Container( - color: isSelected - ? Theme.of(context).colorScheme.primary.withOpacity(0.5) - : Colors.transparent, - padding: EdgeInsets.all(8.0), - child: Align( - alignment: Alignment.topLeft, - child: Text( - level.description, - softWrap: true, - overflow: TextOverflow.visible, - ), - ), - ), - ), - ); - }), - TableCell( - child: Padding( - padding: EdgeInsets.all(8.0), - child: TextField( - controller: remarkControllers[criterion.id], - onChanged: (text) { - remarks[criterion.id] = text; - }, - decoration: InputDecoration( - hintText: 'Enter remark', - border: OutlineInputBorder(), - ), - minLines: 4, - maxLines: 6, - ), - ), - ), - ], - ), - ); - } - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - double minWidth = 800; - double tableWidth = max(minWidth, constraints.maxWidth); - - return SingleChildScrollView( - scrollDirection: Axis.vertical, // Enable vertical scrolling - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, // Enable horizontal scrolling - child: ConstrainedBox( - constraints: BoxConstraints(minWidth: tableWidth), - child: Table( - border: TableBorder.all( - color: Colors.black, - width: 1.0), // Outer border for the table - columnWidths: { - 0: FlexColumnWidth(.5), // Criteria column - for (int i = 1; - i <= rubric!.criteria.first.levels.length; - i++) - i: FlexColumnWidth(1), // Score columns - rubric!.criteria.first.levels.length + 1: - FlexColumnWidth(1.8), // Remarks column - }, - children: tableRows, - ), - ), - ), - ); - }, - ); - } -} diff --git a/team_a/teamA/lib/Views/view_submissions.dart b/team_a/teamA/lib/Views/view_submissions.dart deleted file mode 100644 index 0ed3eca7..00000000 --- a/team_a/teamA/lib/Views/view_submissions.dart +++ /dev/null @@ -1,638 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:learninglens_app/Api/llm/enum/llm_enum.dart'; -import 'package:learninglens_app/Api/llm/grok_api.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:learninglens_app/Api/lms/lms_interface.dart'; -import 'package:learninglens_app/Api/llm/openai_api.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; -import 'package:learninglens_app/beans/submission_with_grade.dart'; -import 'package:learninglens_app/beans/participant.dart'; -import 'view_submission_detail.dart'; -import '../Api/llm/perplexity_api.dart'; -import 'dart:convert'; -import 'package:intl/intl.dart'; - -class SubmissionList extends StatefulWidget { - final int assignmentId; - final String courseId; - - SubmissionList({ - Key? key, - required this.assignmentId, - required this.courseId, - }) : super(key: key); - - @override - SubmissionListState createState() => SubmissionListState(); -} - -class SubmissionListState extends State { - LmsInterface api = LmsFactory.getLmsService(); - Map isLoadingMap = {}; - Map llmSelectionMap = {}; - - late Future> futureSubmissionsWithGrades = - api.getSubmissionsWithGrades(widget.assignmentId); - late Future> futureParticipants = - api.getCourseParticipants(widget.courseId); - - final LlmType defaultLlm = LlmType.PERPLEXITY; - - final perplexityApiKey = LocalStorageService.getPerplexityKey(); - final openApiKey = LocalStorageService.getOpenAIKey(); - final grokApiKey = LocalStorageService.getGrokKey(); - - LlmType? selectedLLM; - - String filterOption = 'All'; - String fullNameFilter = ''; - - String getApiKey(LlmType selectedLLM) { - switch (selectedLLM) { - case LlmType.CHATGPT: - return openApiKey; - case LlmType.GROK: - return grokApiKey; - default: - return perplexityApiKey; - } - } - - @override - void initState() { - super.initState(); - _fetchData(); - } - - void _fetchData() { - setState(() { - futureSubmissionsWithGrades = - api.getSubmissionsWithGrades(widget.assignmentId); - futureParticipants = api.getCourseParticipants(widget.courseId); - }); - } - - void _handleLLMChanged(int participantId, LlmType? newValue) { - setState(() { - if (newValue != null) { - llmSelectionMap[participantId] = newValue; - } - }); - } - - void _handleFilterChanged(String? newValue) { - setState(() { - if (newValue != null) { - filterOption = newValue; - } - }); - } - - void _handleFullNameFilterChanged(String newValue) { - setState(() { - fullNameFilter = newValue; - }); - } - - // Widget to display the "under development" error with icon and back button - Widget _buildUnderDevelopmentError(BuildContext context) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.construction, // Construction icon to indicate "under development" - size: 60, - color: Theme.of(context).colorScheme.error, - ), - SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text( - 'Submissions/Grading feature is currently not available for Google Classroom. Please reach out to the developer for more information.', - style: TextStyle( - color: Theme.of(context).colorScheme.error, - fontSize: 16, - ), - textAlign: TextAlign.center, - ), - ), - SizedBox(height: 24), - ElevatedButton( - onPressed: () { - Navigator.pop(context); // Navigate back to the previous screen - }, - child: Text('Back'), - ), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - child: DropdownButton( - value: filterOption, - onChanged: _handleFilterChanged, - items: ['All', 'With Submission', 'Without Submission'] - .map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - ), - ), - SizedBox(width: 8.0), - Expanded( - child: TextField( - decoration: InputDecoration( - labelText: 'Filter by Name', - border: OutlineInputBorder(), - ), - onChanged: _handleFullNameFilterChanged, - ), - ), - ], - ), - ), - Expanded( - child: Stack( - children: [ - FutureBuilder>( - future: futureParticipants, - builder: (BuildContext context, - AsyncSnapshot> participantSnapshot) { - if (participantSnapshot.connectionState == - ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); - } else if (participantSnapshot.hasError) { - if (participantSnapshot.error is UnimplementedError) { - return _buildUnderDevelopmentError(context); - } - return Center( - child: Text('Error: ${participantSnapshot.error}')); - } else if (!participantSnapshot.hasData || - participantSnapshot.data!.isEmpty) { - return Center(child: Text('No participants found.')); - } else { - return FutureBuilder>( - future: futureSubmissionsWithGrades, - builder: (BuildContext context, - AsyncSnapshot> - submissionSnapshot) { - if (submissionSnapshot.connectionState == - ConnectionState.waiting) { - return Center(child: CircularProgressIndicator()); - } else if (submissionSnapshot.hasError) { - if (submissionSnapshot.error is UnimplementedError) { - return _buildUnderDevelopmentError(context); - } - return Center( - child: - Text('Error: ${submissionSnapshot.error}')); - } else { - List participants = - participantSnapshot.data!; - List submissionsWithGrades = - submissionSnapshot.data ?? []; - - participants.sort((a, b) { - int lastNameComparison = - a.lastname.compareTo(b.lastname); - if (lastNameComparison != 0) { - return lastNameComparison; - } else { - return a.firstname.compareTo(b.firstname); - } - }); - - if (filterOption == 'With Submission') { - participants = participants.where((participant) { - return submissionsWithGrades.any((sub) => - sub.submission.userid == participant.id); - }).toList(); - } else if (filterOption == 'Without Submission') { - participants = participants.where((participant) { - return !submissionsWithGrades.any((sub) => - sub.submission.userid == participant.id); - }).toList(); - } - - if (fullNameFilter.isNotEmpty) { - participants = participants.where((participant) { - return participant.fullname - .toLowerCase() - .contains(fullNameFilter.toLowerCase()); - }).toList(); - } - - return SingleChildScrollView( - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - alignment: WrapAlignment.center, - children: participants.map((participant) { - SubmissionWithGrade? submissionWithGrade = - submissionsWithGrades - .where((sub) => - sub.submission.userid == - participant.id) - .firstOrNull; - - bool isLoading = - isLoadingMap[participant.id] ?? false; - LlmType selectedLLM = llmSelectionMap[ - participant.id] ?? - defaultLlm; - return SizedBox( - width: MediaQuery.of(context).size.width < - 450 - ? double.infinity - : 450, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .secondaryContainer, - border: Border.all( - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - width: 2.0, - ), - borderRadius: - BorderRadius.circular(12.0), - ), - margin: EdgeInsets.symmetric( - vertical: 8, horizontal: 16), - child: Card( - color: Theme.of(context) - .colorScheme - .secondaryContainer, - elevation: 0, - child: ListTile( - leading: CircleAvatar( - backgroundColor: Theme.of(context) - .colorScheme - .onSecondary, - child: Text( - participant.fullname - .substring(0, 1) - .toUpperCase(), - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onSecondaryContainer, - ), - ), - ), - title: Text(participant.fullname), - subtitle: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - if (submissionWithGrade != null) - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - 'Grade Status: ${submissionWithGrade.submission.gradingStatus}'), - Text( - 'Status: ${submissionWithGrade.submission.status}'), - Text( - 'Submitted on: ${DateFormat('MMM d, yyyy h:mm a').format(submissionWithGrade.submission.submissionTime.toLocal())}'), - Text( - 'Grade: ${submissionWithGrade.grade != null ? submissionWithGrade.grade!.grade.toString() : "Not graded yet"}'), - SizedBox(height: 6), - // DropdownButton( - DropdownButtonFormField( - value: selectedLLM, - decoration: InputDecoration( - labelText: 'AI', - ), - onChanged: (newValue) => - _handleLLMChanged( - participant.id, - newValue), - items: LlmType.values - .map((LlmType llm) { - return DropdownMenuItem< - LlmType>( - value: llm, - enabled: - LocalStorageService - .userHasLlmKey( - llm), - child: Text( - llm.displayName, - style: TextStyle( - color: LocalStorageService - .userHasLlmKey( - llm) - ? Colors - .black87 - : Colors.grey, - ), - ), - ); - }).toList(), - ), - SizedBox(height: 4), - ], - ) - else - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox(height: 52), - Text('No Submission', - style: TextStyle( - fontWeight: - FontWeight.bold, - fontSize: 16, - color: Theme.of( - context) - .colorScheme - .error)), - SizedBox(height: 84), - ], - ), - SizedBox(height: 8), - Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - if (submissionWithGrade != - null) - isLoading - ? CircularProgressIndicator() - : ElevatedButton( - onPressed: - () async { - try { - setState(() { - isLoadingMap[ - participant - .id] = - true; - }); - - var submissionText = - submissionWithGrade - .submission - .onlineText; - int? contextId = - await LmsFactory - .getLmsService() - .getContextId( - widget - .assignmentId, - widget - .courseId); - - var fetchedRubric; - if (contextId != - null) { - fetchedRubric = - await LmsFactory - .getLmsService() - .getRubric(widget - .assignmentId - .toString()); - if (fetchedRubric == - null) { - print( - 'Failed to fetch rubric.'); - return; - } - fetchedRubric = - jsonEncode(fetchedRubric - ?.toJson() ?? - {}); - } - - String - queryPrompt = - ''' - I am building a program that generates essay rubric assignments that teachers can distribute to students - who can then submit their responses to be graded. Here is an example format of a rubric roughly: - [ - { - "id": 82, - "rubric_criteria": [ - { - "id": 52, - "description": "Content", - "levels": [ - { - "id": 157, - "score": 1, - "definition": "Poor" - }, - { - "id": 156, - "score": 3, - "definition": "Good" - }, - { - "id": 155, - "score": 5, - "definition": "Excellent" - } - ] - }, - { - "id": 53, - "description": "Clarity", - "levels": [ - { - "id": 160, - "score": 1, - "definition": "Unclear" - }, - { - "id": 159, - "score": 3, - "definition": "Somewhat Clear" - }, - { - "id": 158, - "score": 5, - "definition": "Very Clear" - } - ] - } - ] - } - ] - - I have the following generated essay rubric: - Rubric: $fetchedRubric - - Grade the following submission based on that rubric: - Submission: $submissionText - - You must reply with a representation of the rubric in JSON format that matches this example format, - obviously put your generated scores in and be specific with the remarks on the scoring and give specific examples from the - submitted assignment that were either good or bad depending on the score given. Also cut out anything that is not - the json response. No extraneous comments outside that: - [ - { - "criterionid": 67, - "criterion_description": "Content", - "levelid": 236, - "level_description": "Essay is mostly well-organized, with few issues in flow", - "score": 6, - "remark": "The essay has a clear structure and transitions between paragraphs. Each paragraph focuses on a different aspect of having a park, such as relaxation, activity, and aesthetics. However, there are a few places where the flow could be improved, like the transition between the third and fourth paragraphs." - }, - { - "criterionid": 68, - "criterion_description": "Use of Evidence", - "levelid": 243, - "level_description": "Good use of evidence with occasional gaps", - "score": 6, - "remark": "The essay uses good evidence to support its claims, such as 'Spending time outside can make us feel happier and less anxious, which would help us do better in class.' However, there are occasional gaps where more specific or detailed evidence could strengthen the arguments further." - } - ] - '''; - - String apiKey = - getApiKey( - selectedLLM); - dynamic - llmInstance; - if (selectedLLM == - LlmType - .CHATGPT) { - llmInstance = - OpenAiLLM( - apiKey); - } else if (selectedLLM == - LlmType - .GROK) { - llmInstance = - GrokLLM( - apiKey); - } else { - llmInstance = - PerplexityLLM( - apiKey); - } - dynamic - gradedResponse = - await llmInstance - .postToLlm( - queryPrompt); - gradedResponse = - gradedResponse - .replaceAll( - '```json', - '') - .replaceAll( - '```', - '') - .trim(); - var results = await LmsFactory - .getLmsService() - .setRubricGrades( - widget - .assignmentId, - participant - .id, - gradedResponse); - _fetchData(); - Navigator.push( - context, - MaterialPageRoute( - builder: - (context) => - SubmissionDetail( - participant: - participant, - submission: - submissionWithGrade - .submission, - courseId: widget - .courseId, - ), - ), - ); - print( - 'Results: $results'); - } catch (e) { - print( - 'An error occurred: $e'); - } finally { - setState(() { - isLoadingMap[ - participant - .id] = - false; - }); - } - }, - child: - Text('Grade'), - ), - SizedBox(width: 8), - if (submissionWithGrade != - null) - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - SubmissionDetail( - participant: - participant, - submission: - submissionWithGrade - .submission, - courseId: widget - .courseId, - ), - ), - ); - }, - child: - Text('View Details'), - ), - ], - ), - ], - ), - isThreeLine: true, - ), - ), - ), - ); - }).toList(), - ), - ); - } - }, - ); - } - }, - ), - ], - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/beans/answer.dart b/team_a/teamA/lib/beans/answer.dart deleted file mode 100644 index f953cc89..00000000 --- a/team_a/teamA/lib/beans/answer.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:xml/xml.dart'; -import 'package:learninglens_app/beans/xml_consts.dart'; - -// A single answer for a Question object. Used by all question types except for essay. -class Answer { - String answerText; // Multiple choice text - required - String fraction; // Point value from 0 (incorrect) to 100 (correct) - required - String? feedbackText; // Feedback for the choice - optional - - // Simple constructor. Feedback param is optional. - Answer(this.answerText, this.fraction, [this.feedbackText]); - - // XML factory constructor - factory Answer.fromXml(XmlElement answerElement) { - return Answer( - answerElement.getElement(XmlConsts.text)?.innerText ?? 'UNKNOWN', - answerElement.getAttribute(XmlConsts.fraction) ?? '100', - answerElement - .getElement(XmlConsts.feedback) - ?.getElement(XmlConsts.text) - ?.innerText); - } - - void buildXml(XmlBuilder builder) { - builder.element(XmlConsts.answer, attributes: {XmlConsts.fraction: fraction}, nest: () { - builder.element(XmlConsts.text, nest: answerText); - if (feedbackText != null) { - builder.element(XmlConsts.feedback, nest: () { - builder.element(XmlConsts.text, nest: feedbackText); - }); - } - }); - } - - @override - String toString() { - final sb = StringBuffer(); - sb.write(answerText); - sb.write(' <= ($fraction%)'); - if (feedbackText != null) { - sb.write(' - $feedbackText'); - } - return sb.toString(); - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/beans/assignment.dart b/team_a/teamA/lib/beans/assignment.dart deleted file mode 100644 index bee75071..00000000 --- a/team_a/teamA/lib/beans/assignment.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:learninglens_app/beans/learning_lens_interface.dart'; -import 'package:learninglens_app/beans/submission_with_grade.dart'; - -class Assignment implements LearningLensInterface { - final int? id; - final String name; - final String description; - final DateTime? dueDate; - final DateTime? allowsubmissionsfromdate; - final DateTime? cutoffDate; - final bool isDraft; - final int maxAttempts; - final int gradingStatus; - final int courseId; - - final List? submissionsWithGrades; - - Assignment({ - this.id, - required this.name, - required this.description, - this.dueDate, - this.allowsubmissionsfromdate, - this.cutoffDate, - required this.isDraft, - required this.maxAttempts, - required this.gradingStatus, - required this.courseId, - this.submissionsWithGrades, - }); - - Assignment.empty() - : id = null, - name = '', - description = '', - dueDate = null, - allowsubmissionsfromdate = null, - cutoffDate = null, - isDraft = false, - maxAttempts = 0, - gradingStatus = 0, - courseId = 0, - submissionsWithGrades = null; - - @override - Assignment fromMoodleJson(Map json) { - return Assignment( - id: json['id'] ?? 0, - name: json['name'] ?? 'Untitled', - description: json['description'] ?? json['intro'] ?? '', - dueDate: json['duedate'] != null - ? DateTime.fromMillisecondsSinceEpoch(json['duedate'] * 1000) - : null, - allowsubmissionsfromdate: json['allowsubmissionsfromdate'] != null - ? DateTime.fromMillisecondsSinceEpoch(json['allowsubmissionsfromdate'] * 1000) - : null, - cutoffDate: json['cutoffdate'] != null - ? DateTime.fromMillisecondsSinceEpoch(json['cutoffdate'] * 1000) - : null, - isDraft: json['submissiondrafts'] == 1, - maxAttempts: json['maxattempts'] ?? 0, - gradingStatus: json['gradingstatus'] ?? 0, - courseId: json['course'] ?? 0, - ); - } - - @override - Assignment fromGoogleJson(Map json) { - return Assignment( - id: int.parse(json['id']), - name: json['title'] ?? 'Untitled', - description: json['description'] ?? '', - dueDate: json['dueDate'] != null - ? DateTime(json['dueDate']['year'], - json['dueDate']['month'], - json['dueDate']['day']) - : null, - allowsubmissionsfromdate: json['creationTime'] != null - ? DateTime.parse(json['creationTime']) - : null, - cutoffDate: json['dueDate'] != null - ? DateTime(json['dueDate']['year'], - json['dueDate']['month'], - json['dueDate']['day']) - : null, - isDraft: false, - maxAttempts: 1, - gradingStatus: 0, // TODO: figure out grading status - courseId: int.parse(json['courseId']), - ); - } - @override - String toString() { - return "$name"; - } - -} diff --git a/team_a/teamA/lib/beans/assignment_form.dart b/team_a/teamA/lib/beans/assignment_form.dart deleted file mode 100644 index 611cb6b9..00000000 --- a/team_a/teamA/lib/beans/assignment_form.dart +++ /dev/null @@ -1,27 +0,0 @@ -// Object to pass user-specified parameters to LLM API. -class AssignmentForm { - String? gradingCriteria; - String subject; - String topic; - String gradeLevel; - int maximumGrade; - int? assignmentCount; - int trueFalseCount; - int shortAnswerCount; - int multipleChoiceCount; - String? codingLanguage; - String title; - - AssignmentForm( - {required this.subject, - required this.topic, - required this.gradeLevel, - required this.title, - required this.trueFalseCount, - required this.shortAnswerCount, - required this.multipleChoiceCount, - required this.maximumGrade, - this.assignmentCount, - this.gradingCriteria, - this.codingLanguage}); -} \ No newline at end of file diff --git a/team_a/teamA/lib/beans/course.dart b/team_a/teamA/lib/beans/course.dart deleted file mode 100644 index 52f10961..00000000 --- a/team_a/teamA/lib/beans/course.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:learninglens_app/beans/learning_lens_interface.dart'; -import 'package:learninglens_app/beans/quiz.dart'; -import 'package:learninglens_app/beans/assignment.dart'; - -class Course implements LearningLensInterface { - int id; - String shortName; - String fullName; - DateTime startdate; - DateTime enddate; - String courseId; - String? teacherFolderId; - String? subject; - - List? quizzes; - List? essays; - int? quizTopicId; - int? essayTopicId; - - Course(this.id, this.shortName, this.courseId, this.fullName, this.startdate, - this.enddate, {this.teacherFolderId, this.subject, this.quizzes, this.essays}); - - Course.empty() - : id = 0, - shortName = '', - courseId = '', - fullName = '', - startdate = DateTime.now(), - enddate = DateTime.now(), - teacherFolderId = '', - subject = null, - quizzes = null, - essays = null; - - static String dateFormatted(DateTime date) { - return '${date.month}/${date.day}/${date.year}'; - } - - @override - Course fromMoodleJson(Map json) { - return Course( - json['id'], - json['shortname'] ?? '', - json['idnumber'] ?? '', - json['fullname'] ?? '', - DateTime.fromMillisecondsSinceEpoch(json['startdate'] * 1000), - DateTime.fromMillisecondsSinceEpoch(json['enddate'] * 1000), - subject: json['subject'] ?? 'General', - ); - } - - @override - Course fromGoogleJson(Map json) { - // TODO: Figure out an end date. - return Course( - int.parse(json['id']), - json['name'], - json['section'], - json['name'], - DateTime.parse(json['creationTime']), - DateTime.parse(json['updateTime']).add(Duration(days: 180)), - teacherFolderId: json['teacherFolder']['id'], - ); - } - - @override - String toString() { - return "$shortName ($fullName) $id"; - } -} diff --git a/team_a/teamA/lib/beans/criterion.dart b/team_a/teamA/lib/beans/criterion.dart deleted file mode 100644 index 526e1a9e..00000000 --- a/team_a/teamA/lib/beans/criterion.dart +++ /dev/null @@ -1,38 +0,0 @@ -//A criterion represents a single criteria for use in rubric data -class Criterion -{ - //The name of the criterion, represents the quality being assessed - String name; - //The weight of the criterion, represents the grade percentage of the category - num points; - /// First String in map represents the scale level (e.g. Excellent, Poor) - /// Second String represents the description of the critiera - Map descriptions; - //Holds the default description before it's filled in with scale information - String defaultDesc; - //Defines the available scale descriptions - final List scale3 = ["High", "Moderate", "Low"]; - final List scale4 = ["Outstanding", "Excellent", "Good", "Poor"]; - final List scale5 = [ - "Exceptional", - "Highly effective", - "Effective", - "Inconsistent", - "Unsatisfactory" - ]; - //Creates Criterion object - Criterion(this.name, this.points, this.descriptions, this.defaultDesc); - //Creates Criterion object from JSON asset - Criterion.fromJson(Map json) - : name = json['Name'], - points = 0, - descriptions = {}, - defaultDesc = json['Description']; - - void setWeight(num weight) { - points = weight; - } - void addDescriptions(List scale, List values) { - descriptions = Map.fromIterables(scale, values); - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/beans/file_name_and_bytes.dart b/team_a/teamA/lib/beans/file_name_and_bytes.dart deleted file mode 100644 index 168a9ee1..00000000 --- a/team_a/teamA/lib/beans/file_name_and_bytes.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'dart:typed_data'; - -// Helper bean class for file uploading. -class FileNameAndBytes { - final String filename; - final Uint8List bytes; - - FileNameAndBytes(this.filename, this.bytes); - - @override - String toString() { - return "$filename: ${bytes.lengthInBytes} bytes"; - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/beans/g_question_form_data.dart b/team_a/teamA/lib/beans/g_question_form_data.dart deleted file mode 100644 index e6947192..00000000 --- a/team_a/teamA/lib/beans/g_question_form_data.dart +++ /dev/null @@ -1,27 +0,0 @@ -class QuestionData { - final String question; - final List options; - - QuestionData({required this.question, required this.options}); -} - -// Define a class to hold the form title, questions, and additional assignment details -class FormData { - final String title; - final List questions; - final String? startDate; - final String? endDate; - final String? formUrl; - final String? status; - - FormData({ - required this.title, - required this.questions, - this.startDate, - this.endDate, - this.formUrl, - this.status, - }); -} - - diff --git a/team_a/teamA/lib/beans/grade.dart b/team_a/teamA/lib/beans/grade.dart deleted file mode 100644 index 1a8cb32b..00000000 --- a/team_a/teamA/lib/beans/grade.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:learninglens_app/beans/learning_lens_interface.dart'; - -class Grade implements LearningLensInterface { - final int id; - final int userid; - final double grade; - final int grader; - final DateTime timecreated; - final DateTime timemodified; - - Grade({ - required this.id, - required this.userid, - required this.grade, - required this.grader, - required this.timecreated, - required this.timemodified, - }); - - Grade.empty() - : id = 0, - userid = 0, - grade = 0.0, - grader = 0, - timecreated = DateTime.now(), - timemodified = DateTime.now(); - - @override - Grade fromMoodleJson(Map json) { - return Grade( - id: json['id'] ?? 0, - userid: json['userid'] ?? 0, - grade: json['grade'] != null ? double.parse(json['grade']) : 0.0, - grader: json['grader'] ?? 0, - timecreated: DateTime.fromMillisecondsSinceEpoch(json['timecreated'] * 1000), - timemodified: DateTime.fromMillisecondsSinceEpoch(json['timemodified'] * 1000), - ); - } - - @override - Grade fromGoogleJson(Map json) { - // TODO: Map Google Classroom JSON to Grade. - throw UnimplementedError(); - } -} diff --git a/team_a/teamA/lib/beans/learning_lens_interface.dart b/team_a/teamA/lib/beans/learning_lens_interface.dart deleted file mode 100644 index 21adb0fa..00000000 --- a/team_a/teamA/lib/beans/learning_lens_interface.dart +++ /dev/null @@ -1,8 +0,0 @@ -abstract class LearningLensInterface { - // Force all subclasses to implement this method - LearningLensInterface fromMoodleJson(Map json); - - // TODO: Implement this method - LearningLensInterface fromGoogleJson(Map json); - -} diff --git a/team_a/teamA/lib/beans/lesson_plan.dart b/team_a/teamA/lib/beans/lesson_plan.dart deleted file mode 100644 index 069898b8..00000000 --- a/team_a/teamA/lib/beans/lesson_plan.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:learninglens_app/beans/learning_lens_interface.dart'; - -class LessonPlan implements LearningLensInterface { - final int id; - final String name; - final String intro; - final int timemodified; - - LessonPlan({ - required this.id, - required this.name, - required this.intro, - required this.timemodified, - }); - - // Empty constructor - LessonPlan.empty() - : id = 0, - name = '', - intro = '', - timemodified = 0; - - @override - LessonPlan fromMoodleJson(Map json) { - return LessonPlan( - id: json['id'], - name: json['name'], - intro: json['intro'], - timemodified: json['timemodified'], - ); - } - - @override - LessonPlan fromGoogleJson(Map json) { - print('LessonPlan.fromGoogleJson: $json'); - - return LessonPlan( - id: json['id'] != null ? int.tryParse(json['id'].toString()) ?? 0 : 0, // Ens - name: json['title'] ?? '', - intro: json['description'] ?? '', - timemodified: json['updateTime'] != null - ? DateTime.parse(json['updateTime']).millisecondsSinceEpoch // Convert to int - : 0, - ); - } -} diff --git a/team_a/teamA/lib/beans/level.dart b/team_a/teamA/lib/beans/level.dart deleted file mode 100644 index a72b94b0..00000000 --- a/team_a/teamA/lib/beans/level.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:learninglens_app/beans/learning_lens_interface.dart'; - -class Level implements LearningLensInterface { - final int id; - final String description; - final int score; - - Level({required this.id, required this.description, required this.score}); - - // Empty constructor - Level.empty() - : id = 0, - description = '', - score = 0; - - @override - Level fromMoodleJson(Map json) { - return Level( - id: json['id'] ?? 0, - description: json['definition'] ?? '', - score: json['score'] ?? 0, - ); - } - - @override - Level fromGoogleJson(Map json) { - // TODO: Dinesh, try to map the Google JSON to the Level object - throw UnimplementedError(); - } - - Map toJson() { - return { - 'id': id, - 'description': description, - 'score': score, - }; - } -} diff --git a/team_a/teamA/lib/beans/moodle_rubric.dart b/team_a/teamA/lib/beans/moodle_rubric.dart deleted file mode 100644 index 4604f681..00000000 --- a/team_a/teamA/lib/beans/moodle_rubric.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:learninglens_app/beans/learning_lens_interface.dart'; -import 'package:learninglens_app/beans/moodle_rubric_criteria.dart'; - -class MoodleRubric implements LearningLensInterface { - final String title; - final List criteria; - - MoodleRubric({required this.title, required this.criteria}); - - // Empty constructor - MoodleRubric.empty() - : title = 'Rubric', - criteria = []; - - @override - MoodleRubric fromMoodleJson(Map json) { - var criteriaList = (json['rubric_criteria'] as List) - .map((c) => MoodleRubricCriteria.fromMoodleJson(c)) - .toList(); - - return MoodleRubric( - title: json['criteria_title'] ?? 'Rubric', - criteria: criteriaList, - ); - } - - @override - MoodleRubric fromGoogleJson(Map json) { - // TODO: Dinesh, try to map the Google JSON to the MoodleRubric object and maybe change this class to be more generic - throw UnimplementedError(); - } - - Map toJson() { - return { - 'title': title, - 'criteria': criteria.map((c) => c.toJson()).toList(), - }; - } -} diff --git a/team_a/teamA/lib/beans/moodle_rubric_criteria.dart b/team_a/teamA/lib/beans/moodle_rubric_criteria.dart deleted file mode 100644 index d9a586c1..00000000 --- a/team_a/teamA/lib/beans/moodle_rubric_criteria.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:learninglens_app/beans/level.dart'; - -class MoodleRubricCriteria { - final int id; - final String description; - final List levels; - - MoodleRubricCriteria({required this.id, required this.description, required this.levels}); - - factory MoodleRubricCriteria.fromMoodleJson(Map json) - { - var levelsList = (json['levels'] as List) - .map((l) => Level.empty().fromMoodleJson(l)) - .toList(); - - return MoodleRubricCriteria( - id: json['id'] ?? 0, - description: json['description'] ?? '', - levels: levelsList, - ); - } - - Map toJson() - { - return { - 'id': id, - 'description': description, - 'levels': levels.map((l) => l.toJson()).toList(), - }; - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/beans/override.dart b/team_a/teamA/lib/beans/override.dart deleted file mode 100644 index d41440b6..00000000 --- a/team_a/teamA/lib/beans/override.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:learninglens_app/beans/learning_lens_interface.dart'; - -class Override implements LearningLensInterface { - int id; - String type; - int assignmentId; - String assignmentName; - int courseId; - String courseName; - int userid; - String fullname; - DateTime? endTime; - DateTime? timelimit; - DateTime? cutoffTime; - int? attempts; - - Override( - this.id, - this.type, - this.assignmentId, - this.assignmentName, - this.courseId, - this.courseName, - this.userid, - this.fullname, - this.endTime, - this.timelimit, - this.cutoffTime, - this.attempts, - ); - - Override.empty() - : id = 0, - type = 'quiz', - assignmentId = 0, - assignmentName = '', - courseId = 0, - courseName = '', - userid = 0, - fullname = '', - endTime = null, - timelimit = null, - cutoffTime = null, - attempts = 0; - - @override - Override fromMoodleJson(Map json) { - return Override( - json['override_id'], - json['assignment_type'], - json['assignment_id'], - json['assignment_name'], - json['course_id'], - json['course_name'], - json['userid'], - json['fullname'], - json['end_time'] != null - ? DateTime.fromMillisecondsSinceEpoch(json['end_time'] * 1000) - : null, - json['timelimit'] != null - ? DateTime.fromMillisecondsSinceEpoch(json['timelimit'] * 1000) - : null, - json['cutoff_time'] != null - ? DateTime.fromMillisecondsSinceEpoch(json['cutoff_time'] * 1000) - : null, - json['attempts'] - ); - } - - @override - String toString() { - return "Override(user: $fullname | assignment: $assignmentName | course: $courseName)"; - } - - @override - Override fromGoogleJson(Map json) { - // TODO: Map Google Classroom JSON to Participant. - throw UnimplementedError(); - } -} diff --git a/team_a/teamA/lib/beans/participant.dart b/team_a/teamA/lib/beans/participant.dart deleted file mode 100644 index 2783b50b..00000000 --- a/team_a/teamA/lib/beans/participant.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:learninglens_app/beans/learning_lens_interface.dart'; - -class Participant implements LearningLensInterface { - final int id; - final String fullname; - final String firstname; - final String lastname; - final List roles; - double? avgGrade; - - Participant({ - required this.id, - required this.fullname, - required this.firstname, - required this.lastname, - required this.roles, - this.avgGrade, - }); - - Participant.empty() - : id = 0, - fullname = '', - firstname = '', - lastname = '', - roles = [], - avgGrade = null; - - @override - Participant fromMoodleJson(Map json) { - // Parse roles from the JSON. - List rolesList = []; - if (json['roles'] != null) { - rolesList = (json['roles'] as List) - .map((role) => role['shortname'] as String) - .toList(); - } - return Participant( - id: json['id'] as int, - fullname: json['fullname'] as String, - firstname: json['firstname'] as String, - lastname: json['lastname'] as String, - roles: rolesList, - // Parse avgGrade if available; otherwise, leave as null. - avgGrade: json['avggrade'] != null ? double.tryParse(json['avggrade'].toString()) : null, - ); - } - - @override - String toString() { - return "$fullname"; - } - - @override - Participant fromGoogleJson(Map json) { - // TODO: Map Google Classroom JSON to Participant. - throw UnimplementedError(); - } -} diff --git a/team_a/teamA/lib/beans/question.dart b/team_a/teamA/lib/beans/question.dart deleted file mode 100644 index 8175eee7..00000000 --- a/team_a/teamA/lib/beans/question.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:xml/xml.dart'; -import 'package:learninglens_app/beans/answer.dart'; -import 'package:learninglens_app/beans/xml_consts.dart'; - -// Abstract class that represents a single question. -class Question { - - Question copyWith({String? name, List? answerList, String? type, String? questionText, bool? isFavorite}) => - Question(name: this.name, answerList: this.answerList,type: this.type, questionText: this.questionText, isFavorite: isFavorite ?? this.isFavorite); - - String name; // question name - required. - String type; // question type (multichoice, truefalse, shortanswer, essay) - required. - String questionText; // question text - required. - String? generalFeedback; - String? defaultGrade; - String? responseFormat; - String? responseRequired; - String? attachmentsRequired; - String? responseTemplate; - String? graderInfo; - final bool isFavorite; - // String description; - List answerList = - []; // list of answers. Not needed for essay. - - // Simple constructor. - Question({ - required this.name, - required this.type, - required this.questionText, - this.generalFeedback, - this.defaultGrade, - this.responseFormat, - this.responseRequired, - this.attachmentsRequired, - this.responseTemplate, - this.graderInfo, - this.isFavorite = false, - List? answerList, - }) : answerList = answerList ?? []; - - // XML factory constructor - factory Question.fromXml(XmlElement questionElement) { - Question question = Question( - name: questionElement - .getElement(XmlConsts.name) - ?.getElement(XmlConsts.text) - ?.innerText ?? - 'UNKNOWN', - type: questionElement.getAttribute(XmlConsts.type) ?? XmlConsts.essay, - questionText: questionElement - .getElement(XmlConsts.questiontext) - ?.getElement(XmlConsts.text) - ?.innerText ?? - 'UNKNOWN', - generalFeedback: questionElement - .getElement(XmlConsts.generalfeedback) - ?.getElement(XmlConsts.text) - ?.innerText, - defaultGrade: questionElement.getElement(XmlConsts.defaultgrade)?.innerText, - responseFormat: questionElement.getElement(XmlConsts.responseformat)?.innerText, - responseRequired: questionElement.getElement(XmlConsts.responserequired)?.innerText, - attachmentsRequired: questionElement.getElement(XmlConsts.attachmentsrequired)?.innerText, - responseTemplate: questionElement.getElement(XmlConsts.responsetemplate)?.innerText, - graderInfo: questionElement.getElement(XmlConsts.graderinfo)?.getElement(XmlConsts.text)?.innerText, - ); - - for (XmlElement answerElement - in questionElement.findElements(XmlConsts.answer).toList()) { - question.answerList.add(Answer.fromXml(answerElement)); - } - return question; - } - - set setName(String newname) { - name = newname; - } - -void buildXml(XmlBuilder builder) { - builder.element(XmlConsts.question, attributes: {XmlConsts.type: type}, nest: () { - builder.element(XmlConsts.name, nest: () { - builder.element(XmlConsts.text, nest: name); - }); - - builder.element(XmlConsts.questiontext, nest: () { - builder.element(XmlConsts.text, nest: questionText); - }); - - if (generalFeedback != null) { - builder.element(XmlConsts.generalfeedback, nest: () { - builder.element(XmlConsts.text, nest: generalFeedback); - }); - } - - if (defaultGrade != null) { - builder.element(XmlConsts.defaultgrade, nest: defaultGrade); - } - - if (responseFormat != null) { - builder.element(XmlConsts.responseformat, nest: responseFormat); - } - - if (responseRequired != null) { - builder.element(XmlConsts.responserequired, nest: responseRequired); - } - - if (attachmentsRequired != null) { - builder.element(XmlConsts.attachmentsrequired, nest: attachmentsRequired); - } - - if (responseTemplate != null) { - builder.element(XmlConsts.responsetemplate, nest: responseTemplate); - } - - if (graderInfo != null) { - builder.element(XmlConsts.graderinfo, nest: () { - builder.element(XmlConsts.text, nest: graderInfo); - }); - } - - // Answers - for (var answer in answerList) { - answer.buildXml(builder); - } - }); - } - - @override - String toString() { - final sb = StringBuffer(); - sb.write('$name\n$questionText'); - int charcode = 'A'.codeUnitAt(0); - for (Answer answer in answerList) { - String letter = String.fromCharCode(charcode); - String answerStr = answer.toString(); - sb.write('\n $letter. $answerStr'); - charcode++; - } - return sb.toString(); - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/beans/question_stat_type.dart b/team_a/teamA/lib/beans/question_stat_type.dart deleted file mode 100644 index 2c61e257..00000000 --- a/team_a/teamA/lib/beans/question_stat_type.dart +++ /dev/null @@ -1,35 +0,0 @@ -class QuestionStatsType { - final int id; - final String name; - final String questionText; - final String questionType; - final int numCorrect; - final int numIncorrect; - final int numPartial; - final int totalAttempts; - - QuestionStatsType({ - required this.id, - required this.name, - required this.questionText, - required this.questionType, - required this.numCorrect, - required this.numIncorrect, - required this.numPartial, - required this.totalAttempts, - }); - - // Optional: a factory constructor to build from JSON - factory QuestionStatsType.fromJson(Map json) { - return QuestionStatsType( - id: json['id'] ?? 0, - name: json['name'] ?? '', - questionText: json['questiontext'] ?? '', - questionType: json['qtype'] ?? '', - numCorrect: json['numcorrect'] ?? 0, - numIncorrect: json['numincorrect'] ?? 0, - numPartial: json['numpartial'] ?? 0, - totalAttempts: json['totalattempts'] ?? 0, - ); - } -} diff --git a/team_a/teamA/lib/beans/quiz.dart b/team_a/teamA/lib/beans/quiz.dart deleted file mode 100644 index e05ef45e..00000000 --- a/team_a/teamA/lib/beans/quiz.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:xml/xml.dart'; -import 'package:learninglens_app/beans/question.dart'; -import 'package:learninglens_app/beans/xml_consts.dart'; - -// A Moodle quiz containing a list of questions. -class Quiz { - String? name; // quiz name - optional. - String? description; // quiz description - optional. - List questionList = []; // list of questions on the quiz. - String? promptUsed; - int? id; // quiz id, null if the quiz doesn't exist in Moodle yet - int? coursedId; - - DateTime? timeOpen; - DateTime? timeClose; - // Constructor with all optional params. - Quiz( - {this.name, - this.coursedId, - this.description, - this.id, - this.timeOpen, - this.timeClose, - List? questionList}) - : questionList = questionList ?? []; - - // XML factory constructor using XML string - factory Quiz.fromXmlString(String xmlStr) { - Quiz quiz = Quiz(); - final document = XmlDocument.parse(xmlStr); - final quizElement = document.getElement(XmlConsts.quiz); - - quiz.name = quizElement - ?.getElement(XmlConsts.name) - ?.getElement(XmlConsts.text) - ?.innerText; - - quiz.description = - quizElement!.getElement(XmlConsts.description)?.innerText; - - for (XmlElement questionElement - in quizElement.findElements(XmlConsts.question)) { - if (questionElement.getAttribute(XmlConsts.type) == 'category') { - continue; // Skip category type questions - } - quiz.questionList.add(Question.fromXml(questionElement)); - } - quiz.promptUsed = quizElement.getElement(XmlConsts.promptUsed)?.innerText; - return quiz; - } - - 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']); - - return tmpQuiz; - } - - String toXmlString() { - final builder = XmlBuilder(); - builder.element(XmlConsts.quiz, nest: () { - // Name element - if (name != null) { - builder.element(XmlConsts.name, nest: () { - builder.element(XmlConsts.text, nest: name); - }); - } - - // Description element - if (description != null) { - builder.element(XmlConsts.description, nest: description); - } - - // Insert a "category" type question using the description as the category name - if (description != null) { - builder.element(XmlConsts.question, - attributes: {XmlConsts.type: 'category'}, nest: () { - builder.element(XmlConsts.category, nest: () { - builder.element(XmlConsts.text, - nest: '\$course\$/Top/$description'); - }); - }); - } - - // PromptUsed element - if (promptUsed != null) { - builder.element(XmlConsts.promptUsed, nest: promptUsed); - } - - // Questions - for (var question in questionList) { - question.buildXml(builder); - } - }); - - return builder.buildDocument().toXmlString(pretty: true); - } - - bool isNew() { - return id == null; - } - - @override - String toString() { - final sb = StringBuffer(); - sb.write('Quiz Name: $name\n'); - sb.write('Quiz Description: $description\n\n'); - for (var i = 1; i <= questionList.length; i++) { - sb.write('Q$i: '); - sb.write(questionList[i - 1].toString()); - sb.write('\n\n'); - } - return sb.toString(); - } -} diff --git a/team_a/teamA/lib/beans/quiz_override b/team_a/teamA/lib/beans/quiz_override deleted file mode 100644 index 4a661e1b..00000000 --- a/team_a/teamA/lib/beans/quiz_override +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:learninglens_app/beans/learning_lens_interface.dart'; - -class QuizOverride implements LearningLensInterface { - final int overrideId; - - QuizOverride({required this.overrideId}); - - QuizOverride.empty() : overrideId = 0; - - @override - QuizOverride fromMoodleJson(Map json) { - return QuizOverride( - overrideId: json['overrideid'] as int, - ); - } - - @override - QuizOverride fromGoogleJson(Map json) { - throw UnimplementedError(); - } - - @override - String toString() { - return "QuizOverride(overrideId: $overrideId)"; - } -} diff --git a/team_a/teamA/lib/beans/quiz_type.dart b/team_a/teamA/lib/beans/quiz_type.dart deleted file mode 100644 index c9a88e03..00000000 --- a/team_a/teamA/lib/beans/quiz_type.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:learninglens_app/beans/learning_lens_interface.dart'; - -class QuestionType implements LearningLensInterface { - final int id; - final String name; - final String questionText; - final String questionType; - - QuestionType({ - required this.id, - required this.name, - required this.questionText, - required this.questionType, - }); - - QuestionType.empty() - : id = 0, - name = '', - questionText = '', - questionType = ''; - - @override - QuestionType fromMoodleJson(Map json) { - return QuestionType( - id: json['id'] as int, - name: json['name'] as String? ?? 'Unnamed Question', - questionText: json['questiontext'] as String? ?? 'No Question Text', - questionType: json['qtype'] as String? ?? 'unknown', - ); - } - - @override - QuestionType fromGoogleJson(Map json) { - // TODO: Map Google Classroom JSON to Questions object. - throw UnimplementedError(); - } - - @override - String toString() { - return "Questions(id: $id, name: $name, type: $questionType, text: $questionText)"; - } -} diff --git a/team_a/teamA/lib/beans/rubric.dart b/team_a/teamA/lib/beans/rubric.dart deleted file mode 100644 index 07934807..00000000 --- a/team_a/teamA/lib/beans/rubric.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:xml/xml.dart'; -import 'package:learninglens_app/beans/rubric_criteria.dart'; -import 'package:learninglens_app/beans/xml_consts.dart'; - -// A generated rubric containing criteria for an essay prompt. -// Commented out as the only other instance of Rubric being used is the other one. -class Rubric { - String title; - String subject; - String gradeLevel; - int maxPoints; - List criteriaList; - - Rubric({ - required this.title, - required this.subject, - required this.gradeLevel, - required this.maxPoints, - required this.criteriaList, - }); - - // Factory constructor to create a Rubric from XML - factory Rubric.fromXmlString(String xmlStr) - { - final document = XmlDocument.parse(xmlStr); - final rubricElement = document.getElement(XmlConsts.rubric); - - return Rubric( - title: rubricElement?.getElement(XmlConsts.title)?.innerText ?? 'Untitled', - subject: rubricElement?.getElement(XmlConsts.subject)?.innerText ?? 'Unknown', - gradeLevel: rubricElement?.getElement(XmlConsts.gradeLevel)?.innerText ?? 'Unknown', - maxPoints: int.parse(rubricElement?.getElement(XmlConsts.maxPoints)?.innerText ?? '0'), - criteriaList: rubricElement - ?.findElements(XmlConsts.criteria) - .map((e) => RubricCriteria.fromXml(e)) - .toList() ?? [], - ); - } - - // Convert the Rubric object to an XML string - String toXmlString() { - final builder = XmlBuilder(); - builder.element(XmlConsts.rubric, nest: () { - builder.element(XmlConsts.title, nest: title); - builder.element(XmlConsts.subject, nest: subject); - builder.element(XmlConsts.gradeLevel, nest: gradeLevel); - builder.element(XmlConsts.maxPoints, nest: maxPoints.toString()); - - for (var criteria in criteriaList) { - builder.element(XmlConsts.criteria, nest: criteria.toXml); - } - }); - return builder.buildDocument().toXmlString(pretty: true); - } - - @override - String toString() { - return toXmlString(); - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/beans/rubric_criteria.dart b/team_a/teamA/lib/beans/rubric_criteria.dart deleted file mode 100644 index 60dc2bc8..00000000 --- a/team_a/teamA/lib/beans/rubric_criteria.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:xml/xml.dart'; -import 'package:learninglens_app/beans/xml_consts.dart'; - -// Specific Rubric Criteria -class RubricCriteria { - String description; - int points; - String feedback; - - RubricCriteria({ - required this.description, - required this.points, - this.feedback = '', - }); - - // Factory constructor to create criteria from XML - factory RubricCriteria.fromXml(XmlElement criteriaElement) - { - return RubricCriteria( - description: criteriaElement.getElement(XmlConsts.description)?.innerText ?? 'Unknown', - points: int.parse(criteriaElement.getElement(XmlConsts.points)?.innerText ?? '0'), - feedback: criteriaElement.getElement(XmlConsts.feedback)?.innerText ?? '', - ); - } - - // Convert the criteria to XML format - void toXml(XmlBuilder builder) - { - builder.element(XmlConsts.description, nest: description); - builder.element(XmlConsts.points, nest: points.toString()); - builder.element(XmlConsts.feedback, nest: feedback); - } - - @override - String toString() { - final builder = XmlBuilder(); - toXml(builder); - return builder.buildFragment().toXmlString(); - } -} \ No newline at end of file diff --git a/team_a/teamA/lib/beans/submission.dart b/team_a/teamA/lib/beans/submission.dart deleted file mode 100644 index 7918ca20..00000000 --- a/team_a/teamA/lib/beans/submission.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:learninglens_app/beans/learning_lens_interface.dart'; - -class Submission implements LearningLensInterface { - final int id; - final int userid; - final String status; - final DateTime submissionTime; - final DateTime? modificationTime; - final int attemptNumber; - final int groupId; - final String gradingStatus; - final String onlineText; - final String comments; - final int assignmentId; // Added field - - Submission({ - required this.id, - required this.userid, - required this.status, - required this.submissionTime, - this.modificationTime, - required this.attemptNumber, - required this.groupId, - required this.gradingStatus, - required this.onlineText, - required this.comments, - required this.assignmentId, - }); - - // empty constructor - Submission.empty() - : id = 0, - userid = 0, - status = '', - submissionTime = DateTime.fromMillisecondsSinceEpoch(0), - modificationTime = DateTime.fromMillisecondsSinceEpoch(0), - attemptNumber = 0, - groupId = 0, - gradingStatus = '', - onlineText = '', - comments = '', - assignmentId = 0; - - @override - Submission fromMoodleJson(Map json) { - String onlineText = ''; - String comments = ''; - - // Debug: Print entire submission JSON - // ignore: avoid_print - print('Processing submission: ${json.toString()}'); - int assignmentId = json['assignmentid'] ?? 0; - Map submission = json['submission'] ?? {}; - - if (submission['plugins'] != null && submission['plugins'] is List) { - for (var plugin in submission['plugins']) { - // Extract 'onlineText' - if (plugin['type'] != null && - plugin['type'].toString().toLowerCase() == 'onlinetext') { - var editorFields = plugin['editorfields']; - if (editorFields != null && - editorFields is List && - editorFields.isNotEmpty) { - for (var field in editorFields) { - if (field['name'] != null && - field['name'].toString().toLowerCase() == 'onlinetext') { - onlineText = field['text'] ?? ''; - print('Extracted onlineText: $onlineText'); - break; // Exit loop once the correct field is found - } - } - } - } - - // Extract 'comments' - if (plugin['type'] != null && - plugin['type'].toString().toLowerCase() == 'comments') { - var editorFields = plugin['editorfields']; - if (editorFields != null && - editorFields is List && - editorFields.isNotEmpty) { - for (var field in editorFields) { - if (field['name'] != null && - field['name'].toString().toLowerCase() == 'comments') { - comments = field['text'] ?? ''; - print('Extracted comments: $comments'); - break; // Exit loop once the correct field is found - } - } - } - } - } - } else { - print('No plugins found in submission.'); - } - - return Submission( - id: submission['id'] ?? 0, - userid: submission['userid'] ?? 0, - status: submission['status'] ?? '', - submissionTime: submission['timecreated'] != null - ? DateTime.fromMillisecondsSinceEpoch( - submission['timecreated'] * 1000) - : DateTime.fromMillisecondsSinceEpoch(0), - modificationTime: submission['timemodified'] != null - ? DateTime.fromMillisecondsSinceEpoch( - submission['timemodified'] * 1000) - : null, - attemptNumber: submission['attemptnumber'] ?? 0, - groupId: submission['groupid'] ?? 0, - gradingStatus: submission['gradingstatus'] ?? '', - onlineText: onlineText, - comments: comments, - assignmentId: assignmentId); - } - - @override - Submission fromGoogleJson(Map json) { - // TODO: Dinesh, try to map the Google JSON to the Submission object - throw UnimplementedError(); - } -} diff --git a/team_a/teamA/lib/beans/submission_status.dart b/team_a/teamA/lib/beans/submission_status.dart deleted file mode 100644 index 5b02d98c..00000000 --- a/team_a/teamA/lib/beans/submission_status.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:learninglens_app/beans/learning_lens_interface.dart'; - -class SubmissionStatus implements LearningLensInterface { - final int assignmentId; - final int userId; - final String status; - final DateTime? timeSubmitted; - final DateTime? timeGraded; - final double? grade; - final bool needsGrading; - - SubmissionStatus({ - required this.assignmentId, - required this.userId, - required this.status, - this.timeSubmitted, - this.timeGraded, - this.grade, - required this.needsGrading, - }); - - // Empty constructor - SubmissionStatus.empty() - : assignmentId = 0, - userId = 0, - status = 'unknown', - timeSubmitted = null, - timeGraded = null, - grade = null, - needsGrading = false; - - // Factory method to create a SubmissionStatus object from a JSON response - @override - SubmissionStatus fromMoodleJson(Map json) { - return SubmissionStatus( - assignmentId: json['assignid'] ?? 0, - userId: json['userid'] ?? 0, - status: json['lastattempt']['submission']['status'] ?? 'unknown', - timeSubmitted: json['lastattempt']['submission']['timemodified'] != null - ? DateTime.fromMillisecondsSinceEpoch( - json['lastattempt']['submission']['timemodified'] * 1000) - : null, - timeGraded: json['lastattempt']['grades'] != null && - json['lastattempt']['grades']['grade'] != null - ? DateTime.fromMillisecondsSinceEpoch( - json['lastattempt']['grades']['timemodified'] * 1000) - : null, - grade: json['lastattempt']['grades'] != null && - json['lastattempt']['grades']['grade'] != null - ? double.tryParse(json['lastattempt']['grades']['grade'].toString()) - : null, - needsGrading: json['lastattempt']['gradingstatus'] == 'notgraded', - ); - } - - @override - SubmissionStatus fromGoogleJson(Map json) { - // TODO: Dinesh, try to map the Google JSON to the SubmissionStatus object - throw UnimplementedError(); - } - - // Convert the SubmissionStatus object back to JSON if necessary - Map toJson() { - return { - 'assignid': assignmentId, - 'userid': userId, - 'status': status, - 'timemodified': timeSubmitted?.millisecondsSinceEpoch, - 'timegraded': timeGraded?.millisecondsSinceEpoch, - 'grade': grade, - 'needsgrading': needsGrading, - }; - } -} diff --git a/team_a/teamA/lib/beans/submission_with_grade.dart b/team_a/teamA/lib/beans/submission_with_grade.dart deleted file mode 100644 index 4da4a0f9..00000000 --- a/team_a/teamA/lib/beans/submission_with_grade.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:learninglens_app/beans/submission.dart'; -import 'package:learninglens_app/beans/grade.dart'; - -class SubmissionWithGrade { - final Submission submission; - final Grade? grade; - - SubmissionWithGrade({ - required this.submission, - this.grade, - }); -} \ No newline at end of file diff --git a/team_a/teamA/lib/beans/user.dart b/team_a/teamA/lib/beans/user.dart deleted file mode 100644 index 01c5ba44..00000000 --- a/team_a/teamA/lib/beans/user.dart +++ /dev/null @@ -1,29 +0,0 @@ -class User { - int id; - String firstname; - String lastname; - String email; - - User({ - required this.id, - required this.firstname, - required this.lastname, - required this.email, - }); - - // empty constructor - User.empty() - : id = 0, - firstname = '', - lastname = '', - email = ''; - - factory User.fromJson(Map json) { - return User( - id: json['id'], - firstname: json['firstname'], - lastname: json['lastname'], - email: json['email'] - ); - } -} diff --git a/team_a/teamA/lib/beans/xml_consts.dart b/team_a/teamA/lib/beans/xml_consts.dart deleted file mode 100644 index 7fd68e3b..00000000 --- a/team_a/teamA/lib/beans/xml_consts.dart +++ /dev/null @@ -1,40 +0,0 @@ -// Tags and attributes used in Moodle XML. Useful for preventing typos. -class XmlConsts { - // Quiz tags - static const quiz = 'quiz'; - static const question = 'question'; - static const name = 'name'; - static const description = 'description'; - static const type = 'type'; - static const text = 'text'; - static const questiontext = 'questiontext'; - static const format = 'format'; - static const answer = 'answer'; - static const fraction = 'fraction'; - static const feedback = 'feedback'; - static const generalfeedback = 'generalfeedback'; - static const attachmentsrequired = 'attachmentsrequired'; - static const responseformat = 'responseformat'; - static const responserequired = 'responserequired'; - static const defaultgrade = 'defaultgrade'; - static const responsetemplate = 'responsetemplate'; - static const graderinfo = 'graderinfo'; - static const promptUsed = 'promptused'; - static const category = 'category'; - - // Essay Rubric Tags - static const rubric = 'rubric'; - static const title = 'title'; - static const subject = 'subject'; - static const gradeLevel = 'gradeLevel'; - static const maxPoints = 'maxPoints'; - static const criteria = 'criteria'; - static const points = 'points'; - - // not tags but useful constants - static const multichoice = 'multichoice'; - static const truefalse = 'truefalse'; - static const shortanswer = 'shortanswer'; - static const essay = 'essay'; - static const html = 'html'; -} \ No newline at end of file diff --git a/team_a/teamA/lib/content_carousel.dart b/team_a/teamA/lib/content_carousel.dart deleted file mode 100644 index b580b72e..00000000 --- a/team_a/teamA/lib/content_carousel.dart +++ /dev/null @@ -1,334 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:learninglens_app/Api/lms/enum/lms_enum.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; - -import 'package:learninglens_app/Views/essay_generation.dart'; -import 'package:learninglens_app/Views/g_assignment_create.dart'; -import 'package:learninglens_app/Views/g_quiz_question_page.dart'; - -import 'package:learninglens_app/Views/m_assessment_view.dart'; -import 'package:learninglens_app/Views/quiz_generator.dart'; -import 'package:learninglens_app/Views/essays_view.dart'; -import 'package:learninglens_app/beans/quiz.dart'; -import 'package:learninglens_app/beans/assignment.dart'; -import 'package:learninglens_app/beans/course.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; - -//Provides a carousel of either assessments, essays, or submission -class ContentCarousel extends StatefulWidget { - final String type; - final List? children; - final int? courseId; - - ContentCarousel(this.type, this.children, {this.courseId}); - - @override - State createState() { - return _ContentState(type, children, courseId ?? 0); - } -} - -//State of the carousel (allows for filtering in the future) -class _ContentState extends State { - final String type; - //original list of content - final List _children; - //filtered list to be shown - var children = []; - final int courseId; - _ContentState._(this.type, this._children, this.courseId) { - children = _children; - } - - factory _ContentState(String type, List? input, int? courseId) { - { - //generate the full list of cards - if (type == "assessment") { - return _ContentState._( - type, - CarouselCard.fromQuizzes(input) ?? - [ - Text( - 'There are no generated quizzes that match the requirements.', - style: TextStyle(fontSize: 32)) - ], - courseId ?? 0); - } else if (type == 'essay') { - return _ContentState._( - type, - CarouselCard.fromEssays(input) ?? - [ - Text( - 'This are no generated essays that match the requirements.', - style: TextStyle(fontSize: 32)) - ], - courseId ?? 0); - } - //todo: add submission type - else { - return _ContentState._( - type, [Text('Invalid type input.')], courseId ?? 0); - } - } - } - //todo filtering features - - bool isMoodle() { - print(LocalStorageService.getSelectedClassroom()); - return LocalStorageService.getSelectedClassroom() == LmsType.MOODLE; - } - - @override - Widget build(BuildContext context) { - //For empty contents, we don't build a carousel - if (_children.length == 1 && _children[0].runtimeType == Text) { - return Padding( - padding: EdgeInsets.all(20), - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: 400), - child: Center(child: _children[0]))); - } else { - // return Padding( - // padding: EdgeInsets.symmetric(vertical: 10), - // child: ConstrainedBox( - // constraints: BoxConstraints(maxHeight: 250), //testing width - // child: CarouselView( - // backgroundColor: Theme.of(context).primaryColor, - // itemExtent: 400, - // shrinkExtent: 250, - // onTap: (value) { - // if (type == 'assessment') { - - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => AssessmentsView( - // quizID: (children[value] as CarouselCard).id, - // courseID: - // (children[value] as CarouselCard).courseId)), - // ); - // } else if (type == 'essay') { - // print( - // (children[value] as CarouselCard).courseId?.toString()); - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => EssaysView( - // essayID: (children[value] as CarouselCard).id, - // courseID: (children[value] as CarouselCard) - // .courseId))); - // } - // }, - // children: children, - // ))); - - return Padding( - padding: EdgeInsets.symmetric(vertical: 10), - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: 250), - child: CarouselView( - backgroundColor: Theme.of(context).primaryColor, - itemExtent: 400, - shrinkExtent: 250, - onTap: (value) { - if (type == 'assessment') { - bool isMoodleSelected = isMoodle(); - String courseIdStr = (children[value] as CarouselCard) - .courseId - ?.toString() ?? - ''; - String assessmentIdStr = - (children[value] as CarouselCard).id.toString(); - - print('Course ID I am sending: $courseIdStr'); - print('Assessment ID I am sending: $assessmentIdStr'); - - if (!isMoodleSelected) { - // Assuming "google" is !isMoodle() - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => QuizQuestionPage( - coursedId: courseIdStr, - assessmentId: assessmentIdStr, - ), - ), - ); - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => MAssessmentsView( - quizID: (children[value] as CarouselCard).id, - courseID: - (children[value] as CarouselCard).courseId ?? 0, - ), - ), - ); - } - } else if (type == 'essay') { - print( - (children[value] as CarouselCard).courseId?.toString()); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EssaysView( - essayID: (children[value] as CarouselCard).id, - courseID: (children[value] as CarouselCard) - .courseId))); - } - }, - children: children, - ))); - } - } -} - -//Cards for the Carousel -class CarouselCard extends StatelessWidget { - //assignment name - final String title; - //assignment information (may want to get a specific format, need to look into the various settings for each) - final String information; - //acceptable types: assessment, essay, submission - final String type; - final int id; - final int? courseId; - - CarouselCard(this.title, this.information, this.type, this.id, - {this.courseId}); - - static CarouselCard fromQuiz(Quiz input) { - return CarouselCard( - input.name ?? "Unnamed Quiz", - input.description?.replaceAll(RegExp(r"<[^>]*>"), "") ?? '', - 'assessment', - input.id ?? 0, - courseId: input.coursedId); - } - - static List? fromQuizzes(List? input) { - if (input == null) { - return null; - } - List output = []; - for (Object c in input) { - if (c is Quiz) { - output.insert(output.length, fromQuiz(c)); - } - } - return output; - } - - static CarouselCard fromEssay(Assignment input) { - return CarouselCard( - input.name, - input.description.replaceAll(RegExp(r"<[^>]*>"), ""), - 'essay', - input.id ?? 0, - courseId: input.courseId); - } - - static List? fromEssays(List? input) { - if (input == null) { - return null; - } - List output = []; - for (Object c in input) { - if (c is Assignment) { - output.insert(output.length, fromEssay(c)); - } - } - return output; - } - - @override - Widget build(BuildContext context) { - List? theCourses = LmsFactory.getLmsService().courses; - Course matchedCourse = - theCourses!.firstWhere((element) => element.id == courseId); - return Card( - color: Theme.of(context).colorScheme.secondaryContainer, - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(25.0), // Rounded corners - ), - child: SizedBox( - height: 200, // Adjust this value based on the desired height - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: - Text(title, style: Theme.of(context).textTheme.titleLarge), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text(information), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text('Course: ${matchedCourse.fullName}'), - ), - Spacer(), // Pushes the buttons to the bottom - ], - )), - ); - } -} - -//buttons navigating to the create pages -class CreateButton extends StatelessWidget { - //todo: maybe autofill filter information into the assignment creation settings? - final String filters = ''; - //acceptable types: assessment, essay - final String type; - final String text; - - CreateButton._(this.type, this.text); - - factory CreateButton(String type) { - if (type == "assessment") { - return CreateButton._(type, "Create New Assessment"); - } else if (type == "essay") { - return CreateButton._(type, "Create New Essay Assignment"); - } else { - return CreateButton._(type, ""); - } - } - - bool isMoodle() { - print(LocalStorageService.getSelectedClassroom()); - return LocalStorageService.getSelectedClassroom() == LmsType.MOODLE; - } - - @override - Widget build(BuildContext context) { - return OutlinedButton( - onPressed: () { - MaterialPageRoute? route; - if (type == 'assessment') { - route = MaterialPageRoute(builder: (context) => CreateAssessment()); - } else if (type == 'essay') { - bool isMoodleSelected = isMoodle(); - if (!isMoodleSelected) { - // Assuming "google" is !isMoodle() - - route = MaterialPageRoute( - builder: (context) => CreateAssignmentPage()); - } else { - route = MaterialPageRoute( - builder: (context) => EssayGeneration(title: 'New Essay')); - } - } - if (route != null) { - Navigator.push(context, route); - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [Icon(Icons.add), Text(text)], - )); - } -} diff --git a/team_a/teamA/lib/main.dart b/team_a/teamA/lib/main.dart deleted file mode 100644 index c7ba3ba2..00000000 --- a/team_a/teamA/lib/main.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:learninglens_app/Api/lms/enum/lms_enum.dart'; -import 'package:learninglens_app/Views/assessments_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/local_storage_service.dart'; -import 'package:provider/provider.dart'; -import 'Views/dashboard.dart'; -import 'Views/essay_generation.dart'; -import 'Views/quiz_generator.dart'; -import 'Views/edit_questions.dart'; - - -void main() async{ - await dotenv.load(); - // runApp(MyApp()); - await LocalStorageService.init(); // Initialize SharedPreferences - - runApp( - MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => ThemeNotifier()), // Theme provider - ChangeNotifierProvider(create: (_) => LoginNotifier()), // Login provider - ], - child: MyApp(), - ), - ); -} - -//click and drag for intuitiveness -class CustomScrollBehavior extends ScrollBehavior { - @override - Set get dragDevices => { - PointerDeviceKind.touch, - PointerDeviceKind.mouse, - }; -} - -//below is an app builder, leave it here for now -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - Provider.of(context); - - // used to determine which dashboard to show based on the local storage system - var selectedClassroom = LocalStorageService.getSelectedClassroom(); - var home = selectedClassroom == LmsType.MOODLE ? TeacherDashboard() : TeacherDashboard(); //GoogleTeacherDashboard(); - - return MaterialApp( - debugShowCheckedModeBanner: false, - title: "Learning Lens", - home: home, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Provider.of(context).primaryColor), - ), - scrollBehavior: CustomScrollBehavior(), - routes: { - // '/EssayEditPage': (context) => EssayEditPage(jsonData), - // '/Content': (context) => ViewCourseContents(), - '/EssayGenerationPage': (context) => EssayGeneration(title: 'Essay Generation'), - '/QuizGenerationPage': (context) => CreateAssessment(), - '/EditQuestions': (context) => EditQuestions(''), - // '/create': (context) => const CreatePage(), - '/dashboard': (context) => TeacherDashboard(), - '/user': (context) => UserSettings(), - //'/send_essay_to_moodle': (context) => EssayAssignmentSettings(''), - '/assessments': (context) => AssessmentsView(), - // '/viewExams': (context) => const ViewExamPage(), - // '/settings': (context) => Setting(themeModeNotifier: _themeModeNotifier) - }, - ); - } -} - -class DevLaunch extends StatefulWidget { - @override - State createState() { - return _DevLaunch(); - } -} - -class _DevLaunch extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text('Dev Launch Page')), - body: Column(children: [ - ElevatedButton( - child: const Text('dashboard'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => TeacherDashboard()), - ); - }), - // ElevatedButton( - // child: const Text('Open Edit Essay'), - // onPressed: () { - // Navigator.push( - // context, - // MaterialPageRoute(builder: (context) => EssayEditPage(jsonData)), - // ); - // }), - // ElevatedButton( - // child: const Text('Open Contents Carousel'), - // onPressed: () async { - // if (MoodleApiSingleton().isLoggedIn()){ - // MainController().selectCourse(0); - // } - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => ViewCourseContents()), - // ); - // }), - ElevatedButton( - child: const Text('Open Essay Generation'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - EssayGeneration(title: 'Essay Generation')), - ); - }), - ElevatedButton( - child: const Text('Teacher Dashboard'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => TeacherDashboard()), - ); - }), - // ElevatedButton( - // child: const Text('Send essay to Moodle'), - // onPressed: () { - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => EssayAssignmentSettings(tempRubricXML)), - // ); - // }), - ElevatedButton( - child: const Text('Quiz Generator'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => CreateAssessment())); - }, - ), - ElevatedButton( - child: const Text('Edit Questions'), - onPressed: () { - Navigator.push(context, - MaterialPageRoute(builder: (context) => EditQuestions(''))); - }, - ), - ElevatedButton( - child: const Text('View Quizzes'), - onPressed: (){ - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => AssessmentsView()) - ); - } - ) - ])); - } - -} \ No newline at end of file diff --git a/team_a/teamA/lib/notifiers/login_notifier.dart b/team_a/teamA/lib/notifiers/login_notifier.dart deleted file mode 100644 index d10203ff..00000000 --- a/team_a/teamA/lib/notifiers/login_notifier.dart +++ /dev/null @@ -1,255 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:learninglens_app/Api/lms/factory/lms_factory.dart'; -import 'package:learninglens_app/Api/lms/lms_interface.dart'; -import 'package:learninglens_app/notifiers/login_state.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; - -enum LLMKey { openAI, perplexity, claude, grok } - -class LoginNotifier with ChangeNotifier { - // --------------------------------------- - // New: use these model objects - // --------------------------------------- - final LoginState _moodleState = LoginState(); - final LoginState _googleState = LoginState(); - - // If you want external access to them, you can provide getters: - LoginState get moodleState => _moodleState; - LoginState get googleState => _googleState; - - // You can still store other fields here as needed - bool _hasLLMKey = false; - String? _username; - String? _password; - String? _moodleUrl; - String? _clientID; // for Google - String? _otherError; // If you want any global error, or remove if not needed - - final LmsInterface _api = LmsFactory.getLmsService(); // Moodle API instance - - bool get hasLLMKey => _hasLLMKey; - String? get username => _username; - String? get password => _password; - String? get moodleUrl => _moodleUrl; - - // Constructor - LoginNotifier() { - _loadLoginState(); // Load any saved login state on creation - } - - // --------------------------------------- - // Load from local storage - // --------------------------------------- - Future _loadLoginState() async { - // Moodle - _moodleState.isLoggedIn = LocalStorageService.isLoggedIntoMoodle(); - _username = LocalStorageService.getUsername(); - _password = LocalStorageService.getPassword(); - _moodleUrl = LocalStorageService.getMoodleUrl(); - - // Google - _googleState.isLoggedIn = LocalStorageService.isLoggedIntoGoogle(); - _clientID = LocalStorageService.getGoogleClientId(); - - // LLM Key - _hasLLMKey = await _checkHasLLMKey(); - - // Attempt auto-login if we had credentials - _autoLogin(); - notifyListeners(); - } - - // --------------------------------------- - // Check existence of any LLM keys - // --------------------------------------- - Future _checkHasLLMKey() async { - final openAIKey = LocalStorageService.getOpenAIKey(); - final perplexityKey = LocalStorageService.getPerplexityKey(); - final grokKey = LocalStorageService.getGrokKey(); - - return (openAIKey != null && openAIKey.isNotEmpty) || - (perplexityKey != null && perplexityKey.isNotEmpty) || - (grokKey != null && grokKey.isNotEmpty); - } - - // --------------------------------------- - // Auto-login if we have saved credentials - // --------------------------------------- - Future _autoLogin() async { - if ((_username != null && _username!.isNotEmpty) && - (_password != null && _password!.isNotEmpty) && - (_moodleUrl != null && _moodleUrl!.isNotEmpty)) { - try { - await signInWithMoodle(_username!, _password!, _moodleUrl!); - } catch (e) { - print('Auto-login Error: $e'); - } - } else { - print('Auto-login skipped: Missing or empty credentials.'); - } - } - - // --------------------------------------- - // Moodle: Sign-in - // --------------------------------------- - Future signInWithMoodle(String username, String password, String moodleUrl) async { - try { - 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); - - } else { - // Logged in is false; set a custom error - _moodleState.isLoggedIn = false; - _moodleState.errorMessage = "Invalid username or password."; - } - - notifyListeners(); - } catch (e) { - // Catch the exception, set isLoggedIn = false, set error - _moodleState.isLoggedIn = false; - _moodleState.errorMessage = "Moodle login failed: ${e.toString()}"; - notifyListeners(); - } - } - - // --------------------------------------- - // Moodle: Sign-out - // --------------------------------------- - Future signOutFromMoodle() async { - _moodleState.isLoggedIn = false; - _moodleState.errorMessage = null; - _username = null; - _password = null; - _moodleUrl = null; - - // Clear from local storage - LocalStorageService.clearMoodleLoginState(); - LocalStorageService.clearCredentials(); - LocalStorageService.clearMoodleUrl(); - - // Reset LMS - LmsFactory.getLmsService().resetLMSUserInfo(); - - notifyListeners(); - } - - // --------------------------------------- - // Google: Sign-in - // --------------------------------------- - Future signInWithGoogle() async { - if (_clientID == null) { - throw Exception("GOOGLE_CLIENT_ID not found in .env file."); - } - - try { - await LmsFactory.getLmsServiceGoogle().loginOath(_clientID!); - - // if (_api.isLoggedIn()) { - if (LmsFactory.getLmsServiceGoogle().isLoggedIn()) { - _googleState.isLoggedIn = true; - _googleState.errorMessage = null; - - // Save to local storage - LocalStorageService.saveGoogleLoginState(_googleState.isLoggedIn); - LocalStorageService.saveGoogleAccessToken( - LmsFactory.getLmsServiceGoogle().getGoogleAccessToken() - ); - - notifyListeners(); - } else { - _googleState.isLoggedIn = false; - _googleState.errorMessage = 'Google login failed.'; - notifyListeners(); - throw Exception('Google login failed.'); - } - } catch (e) { - _googleState.isLoggedIn = false; - _googleState.errorMessage = "Google login failed: ${e.toString()}"; - notifyListeners(); - rethrow; // Or remove if you don't want to rethrow - } - } - - // --------------------------------------- - // Google: Sign-out - // --------------------------------------- - Future signOutFromGoogle() async { - try { - LmsFactory.getLmsServiceGoogle().logout(); - _googleState.isLoggedIn = false; - _googleState.errorMessage = null; - - LocalStorageService.clearGoogleLoginState(); - LocalStorageService.clearGoogleAccessToken(); - - notifyListeners(); - } catch (error) { - print("Google Sign-Out Error: $error"); - // Optionally set _googleState.errorMessage - throw Exception("Google Sign-Out failed: $error"); - } - } - - // --------------------------------------- - // Example Classroom API request for Google - // --------------------------------------- - Future makeClassroomApiRequest(String apiEndpoint, dynamic http) async { - final accessToken = LocalStorageService.getGoogleAccessToken(); - - if (accessToken != null) { - try { - final response = await http.get( - Uri.parse(apiEndpoint), - headers: {'Authorization': 'Bearer $accessToken'}, - ); - - if (response.statusCode == 200) { - print('Classroom API Response: ${response.body}'); - } else { - print('Classroom API Error: ${response.statusCode} - ${response.body}'); - throw Exception("Classroom API request failed: ${response.statusCode}"); - } - } catch (e) { - print('Error making Classroom API request: $e'); - throw Exception("Failed to make Classroom API request: $e"); - } - } else { - print('No Google access token available. User needs to sign in.'); - throw Exception("No access token available. Please sign in again."); - } - } - - // --------------------------------------- - // Save the LLM key to local storage - // --------------------------------------- - Future saveLLMKey(LLMKey key, String value) async { - switch (key) { - case LLMKey.openAI: - LocalStorageService.saveOpenAIKey(value); - break; - case LLMKey.perplexity: - LocalStorageService.savePerplexityKey(value); - break; - case LLMKey.grok: - LocalStorageService.saveGrokKey(value); - break; - case LLMKey.claude: - // If you had a Claude key, you could handle it here - break; - } - - _hasLLMKey = await _checkHasLLMKey(); - notifyListeners(); - } -} diff --git a/team_a/teamA/lib/notifiers/login_state.dart b/team_a/teamA/lib/notifiers/login_state.dart deleted file mode 100644 index b5f4f882..00000000 --- a/team_a/teamA/lib/notifiers/login_state.dart +++ /dev/null @@ -1,9 +0,0 @@ -class LoginState { - bool isLoggedIn; - String? errorMessage; - - LoginState({ - this.isLoggedIn = false, - this.errorMessage, - }); -} diff --git a/team_a/teamA/lib/notifiers/theme_notifier.dart b/team_a/teamA/lib/notifiers/theme_notifier.dart deleted file mode 100644 index 5400db49..00000000 --- a/team_a/teamA/lib/notifiers/theme_notifier.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter/material.dart'; - -class ThemeNotifier extends ChangeNotifier { - Color _primaryColor = Colors.deepPurple; - - Color get primaryColor => _primaryColor; - - void updateTheme(Color color) { - _primaryColor = color; - print('Theme updated to: $color'); - notifyListeners(); // Notify listeners (like the whole app) to rebuild - } -} - diff --git a/team_a/teamA/lib/services/api_service.dart b/team_a/teamA/lib/services/api_service.dart deleted file mode 100644 index 61490d33..00000000 --- a/team_a/teamA/lib/services/api_service.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; -import 'package:logger/logger.dart'; - -class ApiService { - // Create a single Logger instance to reuse. - // We read the 'LOGGING_ENABLED' environment variable as a string, - // compare it to 'true', and then decide the log level. - final Logger _logger = Logger( - printer: PrettyPrinter( - methodCount: 0, // Number of stacktrace methods to show - errorMethodCount: 5, // Number of stacktrace methods for errors - colors: true, - printEmojis: true, - printTime: true, - ), - // if we ever want to turn this logging off, we can use the following: - // level: dotenv.env['LOGGING_ENABLED'] == 'true' - // ? Level.verbose - // : Level.nothing, - ); - - /// Sends an HTTP POST request to the specified [url]. - /// - /// Optional parameters: - /// - [headers] for custom headers - /// - [body] for post data - /// - [encoding] to specify the encoding for the request - /// - /// Returns the [http.Response] from the server. - Future httpPost( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) async { - final stopwatch = Stopwatch()..start(); - try { - final response = await http.post( - url, - headers: headers, - body: body, - encoding: encoding, - ); - stopwatch.stop(); - _handleResponse(response, method: 'POST', duration: stopwatch.elapsed); - return response; - } catch (e, stackTrace) { - stopwatch.stop(); - _logger.e( - 'Exception (POST) -> $url (${stopwatch.elapsedMilliseconds}ms)', - e, - stackTrace, - ); - rethrow; - } - - } - - /// Sends an HTTP GET request to the specified [url]. - /// - /// Optional parameters: - /// - [headers] for custom headers - /// - /// Returns the [http.Response] from the server. - Future httpGet( - Uri url, { - Map? headers, - }) async { - final stopwatch = Stopwatch()..start(); - try { - final response = await http.get(url, headers: headers); - stopwatch.stop(); - - _handleResponse(response, method: 'GET', duration: stopwatch.elapsed); - return response; - } catch (e, stackTrace) { - stopwatch.stop(); - _logger.e( - 'Exception (GET) -> $url (${stopwatch.elapsedMilliseconds}ms)', - e, - stackTrace, - ); - rethrow; - } - } - - /// Handles the [response], logging success or error messages. - /// Includes [method] (GET/POST/...) and [duration] for helpful timing info. - void _handleResponse( - http.Response response, { - required String method, - required Duration duration, - }) { - final statusCode = response.statusCode; - final url = response.request?.url; - final ms = duration.inMilliseconds; - - if (statusCode == 200) { - _logger.i( - '[$method] $url -> SUCCESS ($statusCode) in ${ms}ms', - ); - } else { - _logger.w( - '[$method] $url -> ERROR ($statusCode) in ${ms}ms\nBody: ${response.body}', - ); - } - } -} diff --git a/team_a/teamA/lib/services/local_storage_service.dart b/team_a/teamA/lib/services/local_storage_service.dart deleted file mode 100644 index cbbea01e..00000000 --- a/team_a/teamA/lib/services/local_storage_service.dart +++ /dev/null @@ -1,240 +0,0 @@ -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:learninglens_app/Api/lms/enum/lms_enum.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:learninglens_app/Api/llm/enum/llm_enum.dart'; - -/// This class manages local storage operations using SharedPreferences and dotenv. -/// TODO: -/// - Encrypt sensitive data stored in SharedPreferences. -/// - Implement periodic server checks for API availability (Moodle, OpenAI, Claude, Perplexity). -class LocalStorageService { - static late SharedPreferences _prefs; - - /// Initializes SharedPreferences. MUST be called once at app startup. - static Future init() async { - _prefs = await SharedPreferences.getInstance(); - } - - /// Saves user credentials. - static void saveCredentials(String username, String password) { - _prefs.setStringList('credentials', [username, password]); - } - - /// Retrieves stored username, falling back to dotenv. - static String getUsername() { - final credentials = _prefs.getStringList('credentials'); - return credentials != null && credentials.isNotEmpty - ? credentials[0] - : dotenv.env['MOODLE_USERNAME'] ?? ''; - } - - /// Retrieves stored password, falling back to dotenv. - static String getPassword() { - final credentials = _prefs.getStringList('credentials'); - return credentials != null && credentials.length > 1 - ? credentials[1] - : dotenv.env['MOODLE_PASSWORD'] ?? ''; - } - - /// Clears stored credentials. - static void clearCredentials() { - _prefs.remove('credentials'); - } - - /// Saves theme preference. - static void saveTheme(String themeName) { - _prefs.setString('theme', themeName); - } - - /// Retrieves stored theme preference. - static String getTheme() { - return _prefs.getString('theme') ?? 'light'; // Default to 'light' theme - } - - /// Clears theme preference. - static void clearTheme() { - _prefs.remove('theme'); - } - - /// Saves login state. - static void saveMoodleLoginState(bool isLoggedIn) { - _prefs.setBool('isLoggedIntoMoodle', isLoggedIn); - } - - /// Retrieves login state. - static bool isLoggedIntoMoodle() { - return _prefs.getBool('isLoggedIntoMoodle') ?? false; - } - - /// Clears login state. - static void clearMoodleLoginState() { - _prefs.remove('isLoggedIntoMoodle'); - } - - /// Saves login state. - static void saveGoogleLoginState(bool isLoggedIn) { - _prefs.setBool('isLoggedIntoGoogle', isLoggedIn); - } - - /// Retrieves login state. - static bool isLoggedIntoGoogle() { - return _prefs.getBool('isLoggedIntoGoogle') ?? false; - } - - /// Clears login state. - static void clearGoogleLoginState() { - _prefs.remove('isLoggedIntoGoogle'); - } - - /// Saves Moodle URL. - static void saveMoodleUrl(String moodleUrl) { - _prefs.setString('moodleUrl', moodleUrl); - } - - /// Retrieves Moodle URL from storage or dotenv. - static String getMoodleUrl() { - return _prefs.getString('moodleUrl') ?? dotenv.env['MOODLE_URL'] ?? ''; - } - - /// Clears Moodle URL. - static void clearMoodleUrl() { - _prefs.remove('moodleUrl'); - } - - /// Saves primary color. - static void savePrimaryColor(String colorHex) { - _prefs.setString('primaryColor', colorHex); - } - - /// Retrieves primary color. - static String getPrimaryColor() { - return _prefs.getString('primaryColor') ?? '#FFFFFF'; // Default to white - } - - /// Saves OpenAI API key. - static void saveOpenAIKey(String openAIKey) { - _prefs.setString('openAIKey', openAIKey); - } - - /// Retrieves OpenAI API key from storage or dotenv. - static String getOpenAIKey() { - return _prefs.getString('openAIKey') ?? dotenv.env['openai_apikey'] ?? ''; - } - - static bool hasOpenAIKey() { - return getOpenAIKey().isNotEmpty; - } - - /// Clears OpenAI API key. - static void clearOpenAIKey() { - _prefs.remove('openAIKey'); - } - - // /// Saves Claude API key. - // static void saveClaudeKey(String claudeKey) { - // _prefs.setString('claudeKey', claudeKey); - // } - - // /// Retrieves Claude API key from storage or dotenv. - // static String getClaudeKey() { - // return _prefs.getString('claudeKey') ?? dotenv.env['claude_apiKey'] ?? ''; - // } - - // /// Clears Claude API key. - // static void clearClaudeKey() { - // _prefs.remove('claudeKey'); - // } - - /// Saves Perplexity API key. - static void savePerplexityKey(String perplexityKey) { - _prefs.setString('perplexityKey', perplexityKey); - } - - /// Retrieves Perplexity API key from storage or dotenv. - static String getPerplexityKey() { - return _prefs.getString('perplexityKey') ?? dotenv.env['perplexity_apikey'] ?? ''; - } - - static bool hasPerplexityKey() { - return getPerplexityKey().isNotEmpty; - } - - /// Clears Perplexity API key. - static void clearPerplexityKey() { - _prefs.remove('perplexityKey'); - } - - /// Saves Grok API key. - static void saveGrokKey(String grokKey) { - _prefs.setString('grokKey', grokKey); - } - - /// Retrieves Grok API key from storage or dotenv. - static String getGrokKey() { - return _prefs.getString('grokKey') ?? dotenv.env['grok_apiKey'] ?? ''; - } - - static bool hasGrokKey() { - return getGrokKey().isNotEmpty; - } - - /// Clears Grok API key. - static void clearGrokKey() { - _prefs.remove('grokKey'); - } - - static String getGoogleClientId() { - return _prefs.getString('GOOGLE_CLIENT_ID') ?? dotenv.env['GOOGLE_CLIENT_ID'] ?? ''; - } - - static void saveGoogleClientId(String clientId) { - _prefs.setString('GOOGLE_CLIENT_ID', clientId); - } - - static void clearGoogleClientId() { - _prefs.remove('GOOGLE_CLIENT_ID'); - } - - static saveGoogleAccessToken(String accessToken) { - _prefs.setString('GOOGLE_ACCESS_TOKEN', accessToken); - } - - static String? getGoogleAccessToken() { - return _prefs.getString('GOOGLE_ACCESS_TOKEN'); - } - - static clearGoogleAccessToken() { - _prefs.remove('GOOGLE_ACCESS_TOKEN'); - } - // Save LmsType as an INTEGER - static void saveSelectedClassroom(LmsType type) { - _prefs.setInt('selectedClassroom', type.index); - } - - // Get LmsType from stored INTEGER - static LmsType getSelectedClassroom() { - int? storedValue = _prefs.getInt('selectedClassroom'); - return storedValue != null ? LmsType.values[storedValue] : LmsType.MOODLE; - } - - // Clear stored selection - static void clearSelectedClassroom() { - _prefs.remove('selectedClassroom'); - } - - static hasLLMKey() { - return getOpenAIKey().isNotEmpty || getGrokKey().isNotEmpty || getPerplexityKey().isNotEmpty; - } - - static bool userHasLlmKey(LlmType llm) { - if (llm == LlmType.CHATGPT) { - return LocalStorageService.hasOpenAIKey(); - } else if (llm == LlmType.GROK) { - return LocalStorageService.hasGrokKey(); - } else if (llm == LlmType.PERPLEXITY) { - return LocalStorageService.hasPerplexityKey(); - } - - return false; - } -} diff --git a/team_a/teamA/lib/stub/html_stub.dart b/team_a/teamA/lib/stub/html_stub.dart deleted file mode 100644 index a2940b42..00000000 --- a/team_a/teamA/lib/stub/html_stub.dart +++ /dev/null @@ -1,45 +0,0 @@ -// This stub will provide dummy implementations for dart:html classes so that code -// using dart:html can compile on non-web platforms. -class HtmlStyle { - String display = ''; -} - -// A stub for the AnchorElement from dart:html. -class AnchorElement { - AnchorElement({required String href}); - String? download; - // Provide a style property with a default instance. - final HtmlStyle style = HtmlStyle(); - - // Stub for the click() method. - void click() {} - - // Stub for the remove() method. - void remove() {} -} - -// A stub for the Body element. -class Body { - // Provide an append() method that does nothing. - void append(dynamic element) {} -} - -// A stub for the Document. -class Document { - // Provide a dummy body. - final Body? body = Body(); -} - -// A getter for the document. -Document get document => Document(); - -// A stub for the Blob class. -class Blob { - Blob(List parts); -} - -// A stub for the Url helper. -class Url { - static String createObjectUrlFromBlob(Blob blob) => ''; - static void revokeObjectUrl(String url) {} -} diff --git a/team_a/teamA/pubspec.yaml b/team_a/teamA/pubspec.yaml deleted file mode 100644 index 2c933c4b..00000000 --- a/team_a/teamA/pubspec.yaml +++ /dev/null @@ -1,51 +0,0 @@ -name: learninglens_app -description: A new Flutter project. - -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -version: 0.0.1+1 - -environment: - sdk: ^3.1.1 - -dependencies: - flutter: - sdk: flutter - - editable: ^2.0.0 - flutter_dotenv: ^5.1.0 - xml: ^6.2.2 - http: ^1.2.1 - flutter_quill: ^10.8.0 - english_words: ^4.0.0 - provider: ^6.0.0 - cached_network_image: ^3.4.1 - image_network: ^2.5.6 - path: ^1.8.0 - intl: ^0.19.0 - flutter_colorpicker: ^1.1.0 - shared_preferences: ^2.0.8 - logger: ^1.4.0 - google_sign_in: ^6.2.2 - flutter_secure_storage: ^9.2.4 - file_picker: ^9.0.2 - pdf: ^3.10.1 - excel: ^2.0.0 - printing: ^5.10.1 - -dev_dependencies: - flutter_test: - sdk: flutter - - flutter_lints: ^5.0.0 - mockito: ^5.4.2 - build_runner: ^2.4.7 - - -flutter: - assets: - - assets/login_image.png - - .env - - uses-material-design: true - diff --git a/team_a/teamA/test/Views/dashboard_test.dart b/team_a/teamA/test/Views/dashboard_test.dart deleted file mode 100644 index 877ece2c..00000000 --- a/team_a/teamA/test/Views/dashboard_test.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:learninglens_app/Views/dashboard.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -void main() { - setUp(() async { - SharedPreferences.setMockInitialValues({}); - await dotenv.load(fileName: ".env"); - await LocalStorageService.init(); - }); - - testWidgets('Buttons should be disabled when user cannot access the app', (WidgetTester tester) async { - // Manually override static methods - - // except Local - expect(LocalStorageService.isLoggedIntoMoodle(), false); - - await tester.pumpWidget( - const MaterialApp( - home: TeacherDashboard(), - ), - ); - - // Find all buttons - final coursesButton = find.text('Courses'); - final essaysButton = find.text('Essays'); - final iepButton = find.text('IEP'); - final analyticsButton = find.text('Analytics'); - final lessonPlanButton = find.text('Lesson Plan'); - final assessmentsButton = find.text('Assessments'); - - // Verify that all buttons are disabled, to verify this is working. change isNull to isNotNull. - expect(tester.widget(find.ancestor(of: coursesButton, matching: find.byType(ElevatedButton))).onPressed, isNull); - expect(tester.widget(find.ancestor(of: essaysButton, matching: find.byType(ElevatedButton))).onPressed, isNull); - expect(tester.widget(find.ancestor(of: iepButton, matching: find.byType(ElevatedButton))).onPressed, isNull); - expect(tester.widget(find.ancestor(of: analyticsButton, matching: find.byType(ElevatedButton))).onPressed, isNull); - expect(tester.widget(find.ancestor(of: lessonPlanButton, matching: find.byType(ElevatedButton))).onPressed, isNull); - expect(tester.widget(find.ancestor(of: assessmentsButton, matching: find.byType(ElevatedButton))).onPressed, isNull); - }); -} diff --git a/team_a/teamA/test/services/local_storage_service_test.dart b/team_a/teamA/test/services/local_storage_service_test.dart deleted file mode 100644 index 5c63ae72..00000000 --- a/team_a/teamA/test/services/local_storage_service_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:learninglens_app/services/local_storage_service.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; - -// trigger github actions. - - -void main() { - setUpAll(() async { - // Load the dotenv file before running any test - await dotenv.load(fileName: ".env"); - }); - - setUp(() async { - // Set up a mock SharedPreferences instance - SharedPreferences.setMockInitialValues({}); - await LocalStorageService.init(); - }); - - test('saveCredentials stores and retrieves credentials', () { - LocalStorageService.saveCredentials('testUser', 'testPass'); - - expect(LocalStorageService.getUsername(), 'testUser'); - expect(LocalStorageService.getPassword(), 'testPass'); - }); - - test('getUsername defaults to dotenv if no stored credentials', () { - expect(LocalStorageService.getUsername(), dotenv.env['MOODLE_USERNAME'] ?? ''); - }); - - test('clearCredentials removes stored credentials', () { - LocalStorageService.saveCredentials('testUser', 'testPass'); - LocalStorageService.clearCredentials(); - - expect(LocalStorageService.getUsername(), dotenv.env['MOODLE_USERNAME'] ?? ''); - expect(LocalStorageService.getPassword(), dotenv.env['MOODLE_PASSWORD'] ?? ''); - }); - - test('saveTheme and getTheme work correctly', () { - LocalStorageService.saveTheme('dark'); - - expect(LocalStorageService.getTheme(), 'dark'); - }); - - test('saveLoginState and getIsLoggedIn work correctly', () { - LocalStorageService.saveMoodleLoginState(true); - - expect(LocalStorageService.isLoggedIntoMoodle(), true); - }); - - test('clearLoginState resets login state', () { - LocalStorageService.saveMoodleLoginState(true); - LocalStorageService.clearMoodleLoginState(); - - expect(LocalStorageService.isLoggedIntoMoodle(), false); - }); - - test('save and retrieve API keys', () { - LocalStorageService.saveOpenAIKey('test-api-key'); - - expect(LocalStorageService.getOpenAIKey(), 'test-api-key'); - }); - - test('clearOpenAIKey removes stored API key', () { - LocalStorageService.saveOpenAIKey('test-api-key'); - LocalStorageService.clearOpenAIKey(); - - expect(LocalStorageService.getOpenAIKey(), dotenv.env['openai_apikey'] ?? ''); - }); -} diff --git a/team_b/README.md b/team_b/README.md deleted file mode 100644 index 867d5a07..00000000 --- a/team_b/README.md +++ /dev/null @@ -1,9 +0,0 @@ -Team Members for Team B - -Kenneth Jones -Zander Forsythe -Racheal Amuneke -Aly Clark -Bernhard Zwahlen -Patrick Staunton -Brian Klemfuss diff --git a/team_b/yappy/.gitignore b/team_b/yappy/.gitignore deleted file mode 100644 index f8b6864d..00000000 --- a/team_b/yappy/.gitignore +++ /dev/null @@ -1,54 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ -.env -env.g.dart - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release -/android/app/.cxx - -# Ignore native build artifacts -*.cc -*.h -*.cmake -*.swift \ No newline at end of file diff --git a/team_b/yappy/.metadata b/team_b/yappy/.metadata deleted file mode 100644 index 5b39692b..00000000 --- a/team_b/yappy/.metadata +++ /dev/null @@ -1,45 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "d8a9f9a52e5af486f80d932e838ee93861ffd863" - channel: "stable" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - - platform: android - create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - - platform: ios - create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - - platform: linux - create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - - platform: macos - create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - - platform: web - create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - - platform: windows - create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/team_b/yappy/.vscode/settings.json b/team_b/yappy/.vscode/settings.json deleted file mode 100644 index 0e14d8e2..00000000 --- a/team_b/yappy/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.configuration.updateBuildConfiguration": "disabled" -} \ No newline at end of file diff --git a/team_b/yappy/README.md b/team_b/yappy/README.md deleted file mode 100644 index 0ca0757e..00000000 --- a/team_b/yappy/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# yappy! - -yappy! is an application that allows you to store transcripts of recorded audio created by Sherpa within the medical, restaurant, and mechanic industry contexts. These transcripts are then summarized via ChatGPT and parsed for relevant information in those respective industry contexts. Users can then search for specific transcripts and ask an AI assistant for more details about each industry's transcripts. - -### Feature Overview -* Real-time speech-to-text conversion -* Speaker identification -* Industry-specific parsing (restaurant, mechanic, medical) -* Order identification and menu item validation (restaurant) -* Vehicle and part identification (mechanic) -* Visit summarization and question/answer extraction (medical) -* Interactive dashboard with role-based access -* Transcript document import and export capabilities -* Order history review and customer preference tracking - -## Getting Started with yappy! -* Refer to the Programmer's Guide for additional detailed instructions on the structure of the codebase. -* Refer to the Deployment and Operations Guide for environment setup and running yappy! -* Refer to the User Guide for information about how users interact with yappy! - -### Project File Structure Overview -* This project subfolder shall contain the standard Flutter project structure, initially generated using the `flutter new` command. -* UI element code shall be placed in the root of the default “lib” folder. -* Backend code shall be placed in a subfolder under “lib” named “services”. -* Assets, including the icon pack, the database, and test documents shall be placed in the default “assets” folder. -* Unit tests and other UI testing code shall be placed within the default “test” folder. -* Android specific configurations shall be made under the default “android” folder. - -### Database Setup SQL File -[yappy_sql_command_v1.1.txt](https://github.com/user-attachments/files/19398862/yappy_sql_command_v1.1.txt) -To update this file: -1. Delete current database in assets/ directory -2. Delete the database from the emulator (data/data/com.spring2025.yappy/databases/yappy_database.db) - only if the application has been run before -3. Update the SQL file as needed -4. Run SQL commands in a database integrated development environmnent (e.g. SQLite DB Browser) -5. Copy the updated database file to the assets/ directory -6. Update table creation methods and any associated queries in database_helper.dart - -And remember, be **super** happy you have **yappy!** diff --git a/team_b/yappy/analysis_options.yaml b/team_b/yappy/analysis_options.yaml deleted file mode 100644 index d83f5f77..00000000 --- a/team_b/yappy/analysis_options.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -analyzer: - exclude: - - lib/env.dart - - lib/*.g.dart - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/team_b/yappy/android/.gitignore b/team_b/yappy/android/.gitignore deleted file mode 100644 index 55afd919..00000000 --- a/team_b/yappy/android/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java - -# Remember to never publicly share your keystore. -# See https://flutter.dev/to/reference-keystore -key.properties -**/*.keystore -**/*.jks diff --git a/team_b/yappy/android/app/build.gradle b/team_b/yappy/android/app/build.gradle deleted file mode 100644 index 89802d2e..00000000 --- a/team_b/yappy/android/app/build.gradle +++ /dev/null @@ -1,43 +0,0 @@ -plugins { - id "com.android.application" - id "kotlin-android" - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. - id "dev.flutter.flutter-gradle-plugin" -} - -android { - namespace = "com.spring2025.yappy" - compileSdkVersion 35 // Latest stable version - ndkVersion = flutter.ndkVersion - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 - } - - defaultConfig { - applicationId = "com.spring2025.yappy" - minSdkVersion 23 - targetSdkVersion 34 // Latest stable version - versionCode = project.hasProperty('flutterVersionCode') ? flutterVersionCode.toInteger() : 1 - versionName = project.hasProperty('flutterVersionName') ? flutterVersionName : "1.0.0" - } - - buildTypes { - release { - signingConfig = signingConfigs.debug - } - } -} - -flutter { - source = "../.." -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} diff --git a/team_b/yappy/android/app/src/debug/AndroidManifest.xml b/team_b/yappy/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index ab1aac76..00000000 --- a/team_b/yappy/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/team_b/yappy/android/app/src/main/AndroidManifest.xml b/team_b/yappy/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index ae0e391a..00000000 --- a/team_b/yappy/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/team_b/yappy/android/app/src/main/java/com/spring2025/yappy/MainActivity.java b/team_b/yappy/android/app/src/main/java/com/spring2025/yappy/MainActivity.java deleted file mode 100644 index 5193ce97..00000000 --- a/team_b/yappy/android/app/src/main/java/com/spring2025/yappy/MainActivity.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.spring2025.yappy; - -import android.os.Bundle; -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugin.common.MethodChannel; -import android.media.MediaScannerConnection; -import android.net.Uri; - -public class MainActivity extends FlutterActivity { - private static final String CHANNEL = "com.yourcompany.yappy/files"; - - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - super.configureFlutterEngine(flutterEngine); - new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL) - .setMethodCallHandler( - (call, result) -> { - if (call.method.equals("scanFile")) { - String filePath = (String) call.argument("filePath"); - scanFile(filePath); - result.success(null); - } else { - result.notImplemented(); - } - } - ); - } - - private void scanFile(String path) { - MediaScannerConnection.scanFile(this, new String[]{path}, null, (path1, uri) -> { - // File scanned - }); - } -} \ No newline at end of file diff --git a/team_b/yappy/android/app/src/main/kotlin/com/example/yappy/MainActivity.kt b/team_b/yappy/android/app/src/main/kotlin/com/example/yappy/MainActivity.kt deleted file mode 100644 index 6ef3bfbe..00000000 --- a/team_b/yappy/android/app/src/main/kotlin/com/example/yappy/MainActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.yappy - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity: FlutterActivity() diff --git a/team_b/yappy/android/app/src/main/res/drawable-v21/launch_background.xml b/team_b/yappy/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index f74085f3..00000000 --- a/team_b/yappy/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/team_b/yappy/android/app/src/main/res/drawable/launch_background.xml b/team_b/yappy/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f8..00000000 --- a/team_b/yappy/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/team_b/yappy/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/team_b/yappy/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 257944e5..00000000 Binary files a/team_b/yappy/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/team_b/yappy/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/team_b/yappy/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index e1ec93d6..00000000 Binary files a/team_b/yappy/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/team_b/yappy/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/team_b/yappy/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 65985f06..00000000 Binary files a/team_b/yappy/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/team_b/yappy/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/team_b/yappy/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 6eab2408..00000000 Binary files a/team_b/yappy/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/team_b/yappy/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/team_b/yappy/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 5c732d5d..00000000 Binary files a/team_b/yappy/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/team_b/yappy/android/app/src/main/res/values-night/styles.xml b/team_b/yappy/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 06952be7..00000000 --- a/team_b/yappy/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/team_b/yappy/android/app/src/main/res/values/styles.xml b/team_b/yappy/android/app/src/main/res/values/styles.xml deleted file mode 100644 index cb1ef880..00000000 --- a/team_b/yappy/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/team_b/yappy/android/app/src/profile/AndroidManifest.xml b/team_b/yappy/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index ab1aac76..00000000 --- a/team_b/yappy/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/team_b/yappy/android/build.gradle b/team_b/yappy/android/build.gradle deleted file mode 100644 index 660a685c..00000000 --- a/team_b/yappy/android/build.gradle +++ /dev/null @@ -1,32 +0,0 @@ -buildscript { - ext.kotlin_version = '1.8.22' // Latest stable version - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:8.2.1' // Latest stable version - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.gms:google-services:4.3.10' - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = "../build" -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(":app") -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} diff --git a/team_b/yappy/android/gradle.properties b/team_b/yappy/android/gradle.properties deleted file mode 100644 index 25971708..00000000 --- a/team_b/yappy/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError -android.useAndroidX=true -android.enableJetifier=true diff --git a/team_b/yappy/android/gradle/wrapper/gradle-wrapper.properties b/team_b/yappy/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 7bb2df6b..00000000 --- a/team_b/yappy/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip diff --git a/team_b/yappy/android/settings.gradle b/team_b/yappy/android/settings.gradle deleted file mode 100644 index a42444de..00000000 --- a/team_b/yappy/android/settings.gradle +++ /dev/null @@ -1,25 +0,0 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - }() - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.2.1" apply false - id "org.jetbrains.kotlin.android" version "1.8.22" apply false -} - -include ":app" diff --git a/team_b/yappy/assets/icon/app_icon.png b/team_b/yappy/assets/icon/app_icon.png deleted file mode 100644 index 42a3a553..00000000 Binary files a/team_b/yappy/assets/icon/app_icon.png and /dev/null differ diff --git a/team_b/yappy/assets/models_config.json b/team_b/yappy/assets/models_config.json deleted file mode 100644 index 00b4f1c1..00000000 --- a/team_b/yappy/assets/models_config.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "models": [ - { - "id": "speaker_recognition", - "name": "Speaker Recognition Model", - "url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/nemo_en_speakerverification_speakernet.onnx", - "isCompressed": false, - "type": "speaker", - "outputFiles": [ - { - "source": "nemo_en_speakerverification_speakernet.onnx", - "destination": "speaker_model.onnx" - } - ], - "size": 22.3 - }, - { - "id": "silero_vad", - "name": "Voice Activity Detection Model", - "url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad.onnx", - "isCompressed": false, - "type": "vad", - "outputFiles": [ - { - "source": "silero_vad.onnx", - "destination": "vad_model.onnx" - } - ], - "size": 2.2 - }, - { - "id": "whisper_tiny", - "name": "Offline Whisper Model (Tiny)", - "url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-whisper-tiny.en.tar.bz2", - "isCompressed": true, - "type": "offline", - "modelType": "whisper", - "outputFiles": [ - { - "source": "sherpa-onnx-whisper-tiny.en/tiny.en-tokens.txt", - "destination": "offline_tokens.txt" - }, - { - "source": "sherpa-onnx-whisper-tiny.en/tiny.en-decoder.int8.onnx", - "destination": "offline_decoder.onnx" - }, - { - "source": "sherpa-onnx-whisper-tiny.en/tiny.en-encoder.int8.onnx", - "destination": "offline_encoder.onnx" - } - ], - "size": 113.0 - }, - { - "id": "zipformer_online", - "name": "Online Zipformer Model", - "url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-streaming-zipformer-en-20M-2023-02-17-mobile.tar.bz2", - "isCompressed": true, - "type": "online", - "modelType": "zipformer", - "outputFiles": [ - { - "source": "sherpa-onnx-streaming-zipformer-en-20M-2023-02-17-mobile/tokens.txt", - "destination": "online_tokens.txt" - }, - { - "source": "sherpa-onnx-streaming-zipformer-en-20M-2023-02-17-mobile/decoder-epoch-99-avg-1.onnx", - "destination": "online_decoder.onnx" - }, - { - "source": "sherpa-onnx-streaming-zipformer-en-20M-2023-02-17-mobile/encoder-epoch-99-avg-1.int8.onnx", - "destination": "online_encoder.onnx" - }, - { - "source": "sherpa-onnx-streaming-zipformer-en-20M-2023-02-17-mobile/joiner-epoch-99-avg-1.int8.onnx", - "destination": "online_joiner.onnx" - } - ], - "size": 103.0 - } - ] -} \ No newline at end of file diff --git a/team_b/yappy/assets/test_document.txt b/team_b/yappy/assets/test_document.txt deleted file mode 100644 index 68167ea2..00000000 --- a/team_b/yappy/assets/test_document.txt +++ /dev/null @@ -1 +0,0 @@ -This is a Yappy test doc. \ No newline at end of file diff --git a/team_b/yappy/assets/yappy_database.db b/team_b/yappy/assets/yappy_database.db deleted file mode 100644 index d7494f8f..00000000 Binary files a/team_b/yappy/assets/yappy_database.db and /dev/null differ diff --git a/team_b/yappy/ios/.gitignore b/team_b/yappy/ios/.gitignore deleted file mode 100644 index 7a7f9873..00000000 --- a/team_b/yappy/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/team_b/yappy/ios/Flutter/AppFrameworkInfo.plist b/team_b/yappy/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 7c569640..00000000 --- a/team_b/yappy/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 12.0 - - diff --git a/team_b/yappy/ios/Flutter/Debug.xcconfig b/team_b/yappy/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 592ceee8..00000000 --- a/team_b/yappy/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/team_b/yappy/ios/Flutter/Release.xcconfig b/team_b/yappy/ios/Flutter/Release.xcconfig deleted file mode 100644 index 592ceee8..00000000 --- a/team_b/yappy/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/team_b/yappy/ios/Runner.xcodeproj/project.pbxproj b/team_b/yappy/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 92da6d9b..00000000 --- a/team_b/yappy/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,616 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 331C8082294A63A400263BE5 /* RunnerTests */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C8081294A63A400263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C807D294A63A400263BE5 /* Sources */, - 331C807F294A63A400263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C8086294A63A400263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C8080294A63A400263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C8080294A63A400263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C807F294A63A400263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C807D294A63A400263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.yappy; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 331C8088294A63A400263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.yappy.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C8089294A63A400263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.yappy.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C808A294A63A400263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.yappy.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.yappy; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.yappy; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C8088294A63A400263BE5 /* Debug */, - 331C8089294A63A400263BE5 /* Release */, - 331C808A294A63A400263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/team_b/yappy/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/team_b/yappy/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6..00000000 --- a/team_b/yappy/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/team_b/yappy/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/team_b/yappy/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/team_b/yappy/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/team_b/yappy/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/team_b/yappy/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5..00000000 --- a/team_b/yappy/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/team_b/yappy/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/team_b/yappy/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 8e3ca5df..00000000 --- a/team_b/yappy/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/team_b/yappy/ios/Runner.xcworkspace/contents.xcworkspacedata b/team_b/yappy/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16..00000000 --- a/team_b/yappy/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/team_b/yappy/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/team_b/yappy/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/team_b/yappy/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/team_b/yappy/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/team_b/yappy/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5..00000000 --- a/team_b/yappy/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/team_b/yappy/ios/Runner/AppDelegate.swift b/team_b/yappy/ios/Runner/AppDelegate.swift deleted file mode 100644 index 62666446..00000000 --- a/team_b/yappy/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Flutter -import UIKit - -@main -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d0d98aa1..00000000 --- a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1 +0,0 @@ -{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index 1973bf0c..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index a4e21a90..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 75d60ebb..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 87960579..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 0f821066..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index f8bb596f..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index 461ceda0..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 75d60ebb..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c5855fcc..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index 51a766be..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png deleted file mode 100644 index c8d64e0b..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png deleted file mode 100644 index a1d04ff0..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png deleted file mode 100644 index 36be73c2..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png deleted file mode 100644 index 9261c29a..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 51a766be..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 897aca8e..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png deleted file mode 100644 index 257944e5..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png deleted file mode 100644 index 6eab2408..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index 92c86ed1..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 22b147c0..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index 5d71d617..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2f..00000000 --- a/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19eac..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eac..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eac..00000000 Binary files a/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725b..00000000 --- a/team_b/yappy/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/team_b/yappy/ios/Runner/Base.lproj/LaunchScreen.storyboard b/team_b/yappy/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7..00000000 --- a/team_b/yappy/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/team_b/yappy/ios/Runner/Base.lproj/Main.storyboard b/team_b/yappy/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516..00000000 --- a/team_b/yappy/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/team_b/yappy/ios/Runner/Info.plist b/team_b/yappy/ios/Runner/Info.plist deleted file mode 100644 index c90a827b..00000000 --- a/team_b/yappy/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Yappy - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - yappy - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - diff --git a/team_b/yappy/ios/Runner/Runner-Bridging-Header.h b/team_b/yappy/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a56..00000000 --- a/team_b/yappy/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/team_b/yappy/ios/RunnerTests/RunnerTests.swift b/team_b/yappy/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b1..00000000 --- a/team_b/yappy/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Flutter -import UIKit -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/team_b/yappy/lib/audiowave_widget.dart b/team_b/yappy/lib/audiowave_widget.dart deleted file mode 100644 index c1426463..00000000 --- a/team_b/yappy/lib/audiowave_widget.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; -import 'dart:math'; -import 'services/speech_state.dart'; - - -class AudiowaveWidget extends StatelessWidget { - final SpeechState speechState; - - const AudiowaveWidget({super.key, required this.speechState}); - - @override - Widget build(BuildContext context) { - - // Listens for the change in data - return ValueListenableBuilder>( - valueListenable: speechState.audioSamplesNotifier, - builder: (context, samples, child) { - return SizedBox( - height: 100, - width: MediaQuery.of(context).size.width, - child: CustomPaint( - // Paints the audiowave - painter: _WaveformPainter( - samples.isNotEmpty - ? samples - : List.generate(100, (index) => (index % 2 == 0) ? 1000 : -1000), - Colors.deepPurpleAccent, - ), - ), - ); - }, - ); - } -} - -class _WaveformPainter extends CustomPainter { - final List audioSamples; - final Color waveColor; - - _WaveformPainter(this.audioSamples, this.waveColor); - - @override - void paint(Canvas canvas, Size size) { - final Paint paint = Paint() - ..color = waveColor - ..strokeWidth = 2.0 - ..strokeCap = StrokeCap.round; - - // Centers the waves on the screen - final double midY = size.height / 2; - - // Creates spacing - final double sampleSpacing = size.width / max(audioSamples.length, 1); - - // Actually paints the individual lines - for (int i = 0; i < audioSamples.length; i++) { - final double x = i * sampleSpacing; - - // Scale the sample but ensure it doesn't exceed half of the available height - final double maxAmplitude = size.height / 2 - 4; // Subtract 4 for a small margin - - // Normalize then clamp the value between -maxAmplitude and maxAmplitude - double normalizedSample = (audioSamples[i].toDouble() / 32768.0) * maxAmplitude; - normalizedSample = normalizedSample.clamp(-maxAmplitude, maxAmplitude); - - final double yStart = midY - normalizedSample; - final double yEnd = midY + normalizedSample; - - canvas.drawLine(Offset(x, yStart), Offset(x, yEnd), paint); - } - } - - @override - bool shouldRepaint(covariant _WaveformPainter oldDelegate) { - return oldDelegate.audioSamples != audioSamples; - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/contact_page.dart b/team_b/yappy/lib/contact_page.dart deleted file mode 100644 index 5742b3b2..00000000 --- a/team_b/yappy/lib/contact_page.dart +++ /dev/null @@ -1,148 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:yappy/tool_bar.dart'; -import 'package:yappy/theme_provider.dart'; - -//**************************************************************** */ -//**************************************************************** */ -//**************************************************************** */ -//Currently the data doesnt go anywhere. You would need to add an Email address and a way to send the data to that email address -//This is a simple form that takes in a name, email, and message and sends it to an email address -//The user will be notified that the message has been sent -//**************************************************************** */ -//**************************************************************** */ -//**************************************************************** */ - -class ContactApp extends StatelessWidget { - const ContactApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: ContactPage(), - ); - } -} - -class ContactPage extends StatelessWidget { - final TextEditingController nameController = TextEditingController(); - final TextEditingController emailController = TextEditingController(); - final TextEditingController messageController = TextEditingController(); - - ContactPage({super.key}); - - @override - Widget build(BuildContext context) { - final themeProvider = Provider.of(context); - - return Scaffold( - appBar: PreferredSize( - preferredSize: Size.fromHeight(140), - child: ToolBar(), - ), - drawer: HamburgerDrawer(), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Contact Us', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: themeProvider.isDarkMode ? Colors.white : Colors.black, - ), - ), - SizedBox(height: 16), - TextField( - controller: nameController, - decoration: InputDecoration( - labelText: 'Name', - labelStyle: TextStyle( - color: themeProvider.isDarkMode ? Colors.white : Colors.black, - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: themeProvider.isDarkMode ? Colors.white : Colors.black, - ), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.blue), - ), - ), - style: TextStyle(color: themeProvider.isDarkMode ? Colors.white : Colors.black), - ), - SizedBox(height: 16), - TextField( - controller: emailController, - decoration: InputDecoration( - labelText: 'Email', - labelStyle: TextStyle( - color: themeProvider.isDarkMode ? Colors.white : Colors.black, - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: themeProvider.isDarkMode ? Colors.white : Colors.black, - ), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.blue), - ), - ), - style: TextStyle(color: themeProvider.isDarkMode ? Colors.white : Colors.black), - ), - SizedBox(height: 16), - TextField( - controller: messageController, - decoration: InputDecoration( - labelText: 'Message', - labelStyle: TextStyle( - color: themeProvider.isDarkMode ? Colors.white : Colors.black, - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: themeProvider.isDarkMode ? Colors.white : Colors.black, - ), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.blue), - ), - ), - style: TextStyle(color: themeProvider.isDarkMode ? Colors.white : Colors.black), - maxLines: 5, - ), - SizedBox(height: 20), - ElevatedButton( - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Thank You'), - content: Text('Your message has been sent.'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('OK'), - ), - ], - ); - }, - ).then((_) { - // Clear the text fields - nameController.clear(); - emailController.clear(); - messageController.clear(); - }); - }, - child: Text('Send'), - ), - ], - ), - ), - ); - } -} diff --git a/team_b/yappy/lib/env.dart b/team_b/yappy/lib/env.dart deleted file mode 100644 index c4696aa6..00000000 --- a/team_b/yappy/lib/env.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:envied/envied.dart'; - -// part 'env.g.dart'; // Uncomment this line to generate the env.g.dart file - -// Run `flutter pub run build_runner build` to generate the env.g.dart file -@Envied(path: '.env') -abstract class Env { - // Use the following values instead for local testing: - // "_Env.apiKey;" - // "_Env.awsRegion;" - // "_Env.awsAccessKey;" - // "_Env.awsSecretKey;" - // Otherwise, provide an API key within the application's settings while running - - @EnviedField(varName: 'OPENAI_API_KEY') - static String apiKey = ''; - - @EnviedField(varName: 'AWS_REGION') - static const String awsRegion = ''; - - @EnviedField(varName: 'AWS_ACCESS_KEY', obfuscate: true) - static final String awsAccessKey = ''; - - @EnviedField(varName: 'AWS_SECRET_KEY', obfuscate: true) - static final String awsSecretKey = ''; -} diff --git a/team_b/yappy/lib/help.dart b/team_b/yappy/lib/help.dart deleted file mode 100644 index d830c9c6..00000000 --- a/team_b/yappy/lib/help.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:yappy/tool_bar.dart'; -import 'package:yappy/tutorial_page.dart'; -import 'package:yappy/theme_provider.dart'; -import 'package:provider/provider.dart'; - -class HelpApp extends StatelessWidget { - const HelpApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: HelpPage(), - ); - } -} -//Creates a page for the Help industry -//The page will contain the industry menu and the transcription box -class HelpPage extends StatelessWidget { - const HelpPage({super.key}); - - @override - Widget build(BuildContext context) { - final themeProvider = Provider.of(context); - - return Scaffold( - appBar: PreferredSize( - preferredSize: Size.fromHeight(MediaQuery.of(context).size.height * 0.2), - child: ToolBar() - ), - drawer: HamburgerDrawer(), - body: SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Align( - alignment: Alignment.center, - child: Text( - 'Lets Yap about Yappy', - style: TextStyle( - color: themeProvider.isDarkMode ? Colors.white: Colors.black, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Align( - alignment: Alignment.center, - child: Column( - children: [ - Text( - 'Welcome to Yappy! If this is your first time and need help with using Yappy, please select the button below.', - style: TextStyle( - color: themeProvider.isDarkMode ? Colors.white: Colors.black, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 20), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => TutorialPage(), - ), - ); - }, - child: Text('It\'s my first time'), - ), - SizedBox(height: 20), - Text( - 'Reporting a Problem with Yappy\n' - 'If something is not working on Yappy, please follow the instructions below to let us know.\n\n', - style: TextStyle( - color: themeProvider.isDarkMode ? Colors.white: Colors.black, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 5), - ElevatedButton( - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Report a Problem'), - content: Text('Please call: +1-800-123-4567'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('Close'), - ), - ], - ); - }, - ); - }, - child: Text('Report a problem'), - ), - SizedBox(height: 5), - Text( - '\n\nFeedback from the people who use Yappy has helped us redesign our products, improve our policies and fix technical problems. We really appreciate you taking the time to share your thoughts and suggestions with us.\n', - style: TextStyle( - color: themeProvider.isDarkMode ? Colors.white: Colors.black, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 5), - ElevatedButton( - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Feedback for the Help Center'), - - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('Close'), - ), - ], - ); - }, - ); - }, - child: Text('Feedback for the Help Center'), - ), - SizedBox(height: 20), - - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/team_b/yappy/lib/home_page.dart b/team_b/yappy/lib/home_page.dart deleted file mode 100644 index 5609881e..00000000 --- a/team_b/yappy/lib/home_page.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:yappy/contact_page.dart'; -import 'package:yappy/help.dart'; -import 'package:yappy/mechanic.dart'; -import 'package:yappy/medical_doctor.dart'; -import 'package:yappy/medical_patient.dart'; -import 'package:yappy/restaurant.dart'; -import 'package:yappy/tool_bar.dart'; -import 'package:yappy/settings_page.dart'; -import 'package:yappy/tutorial_page.dart'; -import './services/model_manager.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class HomePage extends StatefulWidget { - const HomePage({super.key}); - - @override - State createState() => _HomePageState(); -} - -class _HomePageState extends State { - final ModelManager _modelManager = ModelManager(); - - - Future _checkFirstTimeUser() async { - final preferences = await SharedPreferences.getInstance(); - final isFirstTime = preferences.getBool('is_first_run') ?? true; - - if (isFirstTime && mounted) { - final shouldShowDialog = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Welcome!'), - content: const Text('Is this your first time using the app?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('No'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: const Text('Yes'), - ), - ], - ), - ); - - if (shouldShowDialog != null && shouldShowDialog) { - if (mounted) { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => TutorialPage()), - ); - } - } - - await preferences.setBool('is_first_run', false); - } - } - - Future _checkModelsExist() async { - try { - // Skip check if downloads are already in progress - if (_modelManager.isDownloadInProgress()) { - return; - } - - final modelsExist = await _modelManager.modelsExist(); - if (!modelsExist && mounted) { - // Models don't exist, prompt for download - final shouldDownload = await _modelManager.showDownloadDialog(context); - if (shouldDownload && mounted) { - await _modelManager.downloadModels(context); - } - } - } catch (e) { - debugPrint('Error checking models: $e'); - } - } - - @override - void initState() { - super.initState(); - // Check if models exist after the first frame is rendered - WidgetsBinding.instance.addPostFrameCallback((_) { - _checkModelsExist(); - _checkFirstTimeUser(); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PreferredSize( - preferredSize: Size.fromHeight(MediaQuery.of(context).size.height * 0.2), - child: ToolBar(showHamburger: false), // Using the ToolBar widget - ), - body: SingleChildScrollView( - child: Column( - children: [ - _buildButton('Restaurant', context, RestaurantPage()), - _buildButton('Vehicle Maintenance', context, MechanicalAidPage()), - _buildButton('Medical Doctor', context, MedicalDoctorPage()), - _buildButton('Medical Patient', context, MedicalPatientPage()), - _buildButton('Help', context, HelpPage()), - _buildButton('Contact', context, ContactPage()), - _buildButton('Settings', context, SettingsPage()), - ], - ), - ), - ); - } - - // Function for button navigation - Widget _buildButton(String text, BuildContext context, Widget page) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => page), - ); - }, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 15), - ), - child: Text( - text, - style: const TextStyle(color: Colors.white, fontSize: 16), - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/industry_menu.dart b/team_b/yappy/lib/industry_menu.dart deleted file mode 100644 index c5217aa6..00000000 --- a/team_b/yappy/lib/industry_menu.dart +++ /dev/null @@ -1,807 +0,0 @@ -import 'dart:io'; -import 'dart:convert'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:intl/intl.dart'; -import 'package:record/record.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'main.dart'; -import 'services/openai_helper.dart'; -import 'services/database_helper.dart'; -import 'services/file_handler.dart'; -import 'services/model_manager.dart'; -import 'services/speech_state.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:yappy/services/restaurant_api_module.dart'; -import 'transcript_dialog.dart'; - - -class IndustryMenu extends StatefulWidget { - final String title; - final IconData icon; - final SpeechState speechState; - final ModelManager modelManager; - - const IndustryMenu( - {required this.title, - required this.icon, - super.key, - required this.speechState, - required this.modelManager}); - - @override - State createState() => _IndustryMenuState(); -} - -class _IndustryMenuState extends State { - bool modelsExist = false; - - @override - void initState() { - super.initState(); - _checkModels(); - } - - Future _checkModels() async { - final exist = await widget.modelManager.modelsExist(); - if (mounted) { - setState(() { - modelsExist = exist; - }); - } - } - - // This method generates a transcript dialog including the options for sharing, downloading, and deleting the transcript - Widget generateTranscript( - BuildContext context, String title, String content, int transcript) { - return AlertDialog( - title: Text(title), - content: SingleChildScrollView( - child: Text(content), - ), - actions: [ - //add export capes - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - icon: Icon(Icons.share), - onPressed: () { - // Add your share functionality here - Share.share( - content, - subject: title, - ); - }, - ), - IconButton( - icon: Icon(Icons.download), - onPressed: () async { - // Download button - try { - // Request storage permission - if (await Permission.storage.request().isGranted || - await Permission.manageExternalStorage - .request() - .isGranted) { - // Attempt to find the Downloads directory - final directories = await getExternalStorageDirectories( - type: StorageDirectory.downloads); - final downloadsDirectory = directories?.first; - - if (downloadsDirectory != null) { - final filePath = '${downloadsDirectory.path}/$title.txt'; - final file = File(filePath); - await file.writeAsBytes(utf8.encode(content), - flush: true); - - await MethodChannel('com.yourcompany.yappy/files') - .invokeMethod('scanFile', {'filePath': filePath}); - - FileHandler fileHandler = FileHandler(); - await fileHandler.addDocument(file); - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Transcript saved to $filePath')), - ); - } - debugPrint('File saved at: $filePath'); - } else { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text('Failed to find Downloads directory')), - ); - } - } - } else { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Storage permission denied')), - ); - } - } - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to save file: $e')), - ); - } - debugPrint('Failed to save file: $e'); - } - }, - ), - IconButton( - icon: Icon(Icons.delete), - onPressed: () async { - // Add your delete functionality here - bool confirmDelete = await showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Confirm Delete'), - content: Text( - 'Are you sure you want to delete this transcript?'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(true); - }, - child: Text('Delete'), - ), - ], - ); - }, - ); - - if (confirmDelete) { - // Perform the delete operation - await DatabaseHelper().deleteTranscript(transcript); - if (!context.mounted) return; - Navigator.of(context).pop(); - } - }, - ), - ], - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('Close'), - ), - ], - ); - } - - // This method fetches all transcripts from the database - Future>> _fetchTranscripts() async { - DatabaseHelper dbHelper = DatabaseHelper(); - return await dbHelper.getAllTranscripts(); - } - - // This method builds the industry menu widget where the user can record, view transcripts, and view transcript history - // Added the fourth button for Chat Bot - @override - Widget build(BuildContext context) { - // Gets the width and height of the current screen - double screenWidth = MediaQuery.of(context).size.width; - double screenHeight = MediaQuery.of(context).size.height; - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - - // Creates a column for the items within the menu - child: Column( - children: [ - Center( - // Creates the text box above the icons - child: Container( - width: screenWidth * .75, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: isDarkMode ? Color.fromARGB(255, 79, 79, 83): Colors.green, - ), - padding: EdgeInsets.all(12), - child: Center( - child: Text( - widget.title, - style: TextStyle(fontSize: 24, color: Colors.white), - ), - ), - )), - - SizedBox(height: screenHeight * .03), - - // Creates a row of clickable menu icons - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Creates the chat button for each menu - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: !modelsExist - ? Color.fromRGBO(128, 128, 128, 0.5) - : (widget.speechState.recordState == RecordState.stop - ? isDarkMode ? Colors.grey : Colors.green - : Colors.red)), - padding: EdgeInsets.all(5), - child: Tooltip( - message: !modelsExist - ? "Download required models to enable recording" - : (widget.speechState.recordState == RecordState.stop - ? "Start recording" - : "Stop recording"), - child: IconButton( - icon: Icon( - widget.speechState.recordState == RecordState.stop - ? Icons.mic - : Icons.stop, - color: !modelsExist - ? isDarkMode ? Colors.grey : Colors.green - : Colors.white, - size: screenHeight * .05, - ), - onPressed: !modelsExist - ? null - : () async { - await widget.speechState.toggleRecording(); - - if (widget.speechState.recordState == RecordState.stop) { - // Fetch both transcripts - String localTranscript = widget.speechState.getRecordedText(); - String awsTranscript = widget.speechState.getAwsRecordedText(); - bool awsAvailable = await preferences.setBool('is_aws_available', true); - - // Get the user ID - int userId = 0001; - - // Create a new transcript ID - int transcriptId = DateTime.now().millisecondsSinceEpoch; - - // Show the dialog with both transcripts - if (!context.mounted) return; - showDialog( - context: context, - builder: (BuildContext context) { - return TranscriptDialog( - localTranscript: localTranscript, - awsTranscript: awsTranscript, - awsAvailable: awsAvailable, - userId: userId, - transcriptId: transcriptId, - industry: widget.title, - onSave: (userId, transcriptId, text, industry) async { - // Save the selected transcript to the database - await DatabaseHelper().saveTranscript( - userId: userId, - transcriptId: transcriptId, - text: text, - industry: industry, - ); - - // Kick off the AI summarization process - var openAIHelper = OpenAIHelper(); - String aiResponse = ''; - try { - aiResponse = await openAIHelper.summarizeTranscription( - userId, industry, transcriptId - ); - } catch (e) { - // Lets the user know that transcription summarization failed - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to summarize transcription: $e')), - ); - } - } - - debugPrint(aiResponse); - }, - ); - }, - ); - } - }, - ), - ), - ), - SizedBox(width: screenWidth * .06), - - // Creates a industry specific icon based on user input - Container( - decoration: - BoxDecoration(shape: BoxShape.circle, color: isDarkMode ? Colors.grey : Colors.green), - padding: EdgeInsets.all(5), - child: IconButton( - icon: Icon( - widget.icon, - color: Colors.white, - size: screenHeight * .05, - ), - onPressed: () { - _showTranscriptsBottomSheet(context); - }, - ), - ), - SizedBox(width: screenWidth * .06), - - // Creates a transcript history button - Container( - decoration: - BoxDecoration(shape: BoxShape.circle, color: isDarkMode ? Colors.grey : Colors.green), - padding: EdgeInsets.all(5), - child: IconButton( - icon: Icon( - Icons.file_copy, - color: Colors.white, - size: screenHeight * .05, - ), - onPressed: () { - _showTranscriptsHistoryBottomSheet(context); - }, - ), - ), - - SizedBox(width: screenWidth * .06), - - // Creates a chatbot button - Container( - decoration: BoxDecoration(shape: BoxShape.circle, color: isDarkMode ? Colors.grey : Colors.green), - padding: EdgeInsets.all(5), - child: IconButton( - icon: Icon( - Icons.android, - color: Colors.white, - size: screenHeight * .05, - ), - onPressed: () async { - // Kick off the AI chat session - var openAIHelper = OpenAIHelper(); - TextEditingController chatController = TextEditingController(); - String? aiResponse = ''; - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Ask Yappy about stored transcripts'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: chatController, - decoration: InputDecoration(hintText: 'Enter a question'), - ), - ], - ), - actions: [ - TextButton( - onPressed: () async { - String userInput = chatController.text; - if (userInput.isNotEmpty) { - // Show intermediary dialog - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Generating Response'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircularProgressIndicator(), - SizedBox(height: 20), - Text('Please wait while the response is being generated...'), - ], - ), - ); - }, - ); - - aiResponse = await openAIHelper.startTranscriptChatAssistant(userInput, widget.title); - if (!context.mounted) return; - Navigator.of(context).pop(); // Close the intermediary dialog - Navigator.of(context).pop(); // Close the first dialog - if (aiResponse != null) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Yappy Response'), - content: SingleChildScrollView( - child: Text(aiResponse ?? "No response"), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('Close'), - ), - ], - ); - }, - ); - } - } - }, - child: Text('Submit'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('Cancel'), - ), - ], - ); - }, - ); - }, - ), - ), - ], - ), - ], - ), - ); - } - - // Extract the functionality to show transcripts into a separate method - void _showTranscriptsBottomSheet(BuildContext context) async { - // Fetch transcripts first - List> transcripts = await _fetchTranscripts(); - if (!context.mounted) return; - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - // Check if the context is still valid - if (!context.mounted) return; - - showModalBottomSheet( - context: context, - builder: (BuildContext context) { - return Container( - padding: EdgeInsets.all(16.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: isDarkMode ? const Color.fromARGB(255, 67, 67, 67) : Colors.green, - ), - child: Center( - child: Column( - children: [ - Expanded( - child: ListView.builder( - itemCount: transcripts.length, - itemBuilder: (context, index) { - Map transcript = transcripts[index]; - if (transcript['industry'] == widget.title) { - return ListTile( - title: Text( - // Format the transcript ID to Day Month Year Time - DateFormat('dd MMM yyyy HH:mm').format( - DateTime.fromMillisecondsSinceEpoch( - transcript['transcript_id'])), - style: TextStyle(color: Colors.white), - ), - onTap: () async { - Navigator.pop(context); - if (widget.title == 'Restaurant') { - // Fetch the AI response from the database - List validatedMenuItems = []; - - try { - final aiResponse = transcript['transcript_ai_response']; - String parsedResponse = aiResponse.replaceAll(RegExp(r'\[OpenAIChatCompletionChoiceMessageContentItemModel\(type: text, text: '), '').replaceAll(RegExp(r'\)\]'), ''); - List parsedResponseList = parsedResponse.split('\n').where((item) => item.trim().isNotEmpty).toList(); - // [OpenAIChatCompletionChoiceMessageContentItemModel(type: text, text: Seat 1: Burger, fries, and a soda. - // I/flutter (17322): - // I/flutter (17322): Seat 2: Burger, fries, and a soda. - // I/flutter (17322): - // I/flutter (17322): Seat 3: Burger, fries, and a soda. - // I/flutter (17322): - // I/flutter (17322): Seat 4: Double burger with large fries and a soda.)] - var restaurantAPI = RestaurantAPI(); - validatedMenuItems = await restaurantAPI.validateMenuItems(parsedResponseList); - if (!context.mounted) return; - showModalBottomSheet( - context: context, - builder: (BuildContext context) { - - return KanbanBoard(tasks: validatedMenuItems); - }, - ); - } catch (e) { - // Handle API call failure - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to validate menu items: $e')), - ); - } - } - } else { - final aiResponse = transcript['transcript_ai_response']; - String parsedResponse = aiResponse.replaceAll(RegExp(r'\[OpenAIChatCompletionChoiceMessageContentItemModel\(type: text, text: '), '').replaceAll(RegExp(r'\)\]'), ''); - // Show regular transcript for other industries - // Show the parsedResponse in a regular dialog with a close button at the bottom - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Transcript'), - content: SingleChildScrollView( - child: Text(parsedResponse), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('Close'), - ), - ], - ); - }, - ); - } - }, - ); - } else { - return SizedBox.shrink(); - } - }, - ), - ), - ], - ), - ), - ); - }, - ); - } - - - - // Extract the functionality to show transcript history into a separate method - void _showTranscriptsHistoryBottomSheet(BuildContext context) async { - // Fetch transcripts first - List> transcripts = await _fetchTranscripts(); - if (!context.mounted) return; - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - // Check if the context is still valid - if (!context.mounted) return; - - showModalBottomSheet( - context: context, - builder: (BuildContext context) { - return Container( - padding: EdgeInsets.all(16.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: isDarkMode ? const Color.fromARGB(255, 67, 67, 67) : Colors.green, - ), - child: Center( - child: Column( - children: [ - Expanded( - child: ListView.builder( - itemCount: transcripts.length, - itemBuilder: (context, index) { - Map transcript = transcripts[index]; - if (transcript['industry'] == widget.title) { - return ListTile( - title: Text( - // Format the transcript ID to Day Month Year Time - DateFormat('dd MMM yyyy HH:mm').format( - DateTime.fromMillisecondsSinceEpoch( - transcript['transcript_id'])), - style: TextStyle(color: Colors.white), - ), - onTap: () { - Navigator.pop(context); - showDialog( - context: context, - builder: (BuildContext context) { - return generateTranscript( - context, - 'Transcript', - transcript['transcript_text_data'] ?? - 'No content available', - transcript['transcript_id'], - ); - }, - ); - }, - ); - } else { - return SizedBox.shrink(); - } - }, - ), - ), - // Add an upload bar at the bottom - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: ElevatedButton.icon( - onPressed: () async { - // Upload button - FilePickerResult? result = - await FilePicker.platform.pickFiles(); - if (result != null) { - PlatformFile file = result.files.first; - String uploadedFileName = file.name; - String uploadedFileContent = - await File(file.path!).readAsString(); - - await DatabaseHelper().saveTranscript( - userId: 0001, - transcriptId: DateTime.now().millisecondsSinceEpoch, - text: uploadedFileContent, - industry: widget.title); - - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text('File uploaded: $uploadedFileName')), - ); - } else { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('File upload canceled')), - ); - } - }, - icon: Icon(Icons.upload), - label: Text('Upload Transcript'), - style: ElevatedButton.styleFrom( - backgroundColor: const Color.fromARGB(255, 26, 26, 27), - foregroundColor: const Color.fromARGB(255, 229, 217, 217), - ), - ), - ), - ], - ), - ), - ); - }, - ); - } -} - -class KanbanBoard extends StatefulWidget { - final List tasks; - - const KanbanBoard({super.key, required this.tasks}); - - @override - KanbanBoardState createState() => KanbanBoardState(); -} - -class KanbanBoardState extends State { - late List tasks; - - @override - void initState() { - super.initState(); - tasks = widget.tasks; - } - - //creates the kanban board sytle widget for the restaurant - //need to allow the user to edit the order by holding the card - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Restaurant Order'), - ), - body: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - Expanded( - child: ListView.builder( - itemCount: tasks.length, - itemBuilder: (context, index) { - return Card( - elevation: 4, - margin: const EdgeInsets.symmetric(vertical: 8), - child: ListTile( - title: Text(tasks[index]), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon(Icons.arrow_upward), - onPressed: index > 0 - ? () { - setState(() { - final temp = tasks[index]; - tasks[index] = tasks[index - 1]; - tasks[index - 1] = temp; - }); - } - : null, - ), - IconButton( - icon: Icon(Icons.arrow_downward), - onPressed: index < tasks.length - 1 - ? () { - setState(() { - final temp = tasks[index]; - tasks[index] = tasks[index + 1]; - tasks[index + 1] = temp; - }); - } - : null, - ), - IconButton( - icon: Icon(Icons.edit), - onPressed: () { - TextEditingController controller = - TextEditingController(text: tasks[index]); - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Edit Task'), - content: TextField( - controller: controller, - decoration: InputDecoration( - hintText: 'Enter new task'), - ), - actions: [ - TextButton( - onPressed: () { - setState(() { - tasks[index] = controller.text; - }); - Navigator.of(context).pop(); - }, - child: Text('Save'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('Cancel'), - ), - ], - ); - }, - ); - }, - ), - ], - ), - ), - ); - }, - ), - ), - // Add a close button at the bottom - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('Close'), - ), - ], - ), - ), - ); - } -} diff --git a/team_b/yappy/lib/login_page.dart b/team_b/yappy/lib/login_page.dart deleted file mode 100644 index cb14275f..00000000 --- a/team_b/yappy/lib/login_page.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'package:flutter/material.dart'; -import 'sign_up_page.dart'; // Import the SignUpPage - -class LoginPage extends StatefulWidget { - const LoginPage({super.key}); - - @override - LoginPageState createState() => LoginPageState(); -} - -class LoginPageState extends State { - final TextEditingController _usernameController = TextEditingController(); - final TextEditingController _passwordController = TextEditingController(); - - @override - void dispose() { - _usernameController.dispose(); - _passwordController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.black, - body: Center( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CircleAvatar( - backgroundColor: Colors.white, - radius: 30, - child: Icon(Icons.chat, color: Colors.green, size: 40), - ), - const SizedBox(height: 20), - - // Login Button - ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 80), - ), - child: const Text( - 'Login', - style: TextStyle(color: Colors.white, fontSize: 18), - ), - ), - const SizedBox(height: 20), - - // Username TextField - TextField( - controller: _usernameController, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - filled: true, - fillColor: Colors.grey[900], - border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), - labelText: 'Username', - labelStyle: const TextStyle(color: Colors.white), - ), - ), - const SizedBox(height: 10), - - // Password TextField - TextField( - controller: _passwordController, - obscureText: true, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - filled: true, - fillColor: Colors.grey[900], - border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), - labelText: 'Password', - labelStyle: const TextStyle(color: Colors.white), - ), - ), - const SizedBox(height: 20), - - // Submit Button - ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 40), - ), - child: const Text( - 'Submit', - style: TextStyle(color: Colors.white, fontSize: 16), - ), - ), - const SizedBox(height: 30), - - // Terms and Conditions - const Text( - 'Terms and Conditions', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16), - ), - const SizedBox(height: 5), - const Text( - 'Ensure consent is obtained before using the application, as required in certain states.', - textAlign: TextAlign.center, - style: TextStyle(color: Colors.white60, fontSize: 12), - ), - const SizedBox(height: 20), - - // Sign-up Button with Navigation - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => SignUpPage()), // Navigate to SignUpPage - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 60), - ), - child: const Text( - 'Sign-up', - style: TextStyle(color: Colors.white, fontSize: 16), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/team_b/yappy/lib/main.dart b/team_b/yappy/lib/main.dart deleted file mode 100644 index 194c0075..00000000 --- a/team_b/yappy/lib/main.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'dart:io' show Platform; -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:dart_openai/dart_openai.dart'; -import 'package:sqflite_common_ffi/sqflite_ffi.dart'; -import './home_page.dart'; -import './services/database_helper.dart'; -import './env.dart'; -import './toast_widget.dart'; -import 'package:provider/provider.dart'; -import 'theme_provider.dart'; - -// Create a global instance of DatabaseHelper -final DatabaseHelper dbHelper = DatabaseHelper(); -final GlobalKey navigatorKey = GlobalKey(); -late SharedPreferences preferences; - -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - preferences = await SharedPreferences.getInstance(); - - // Load the theme preference from SharedPreferences - final isDarkMode = preferences.getBool('toggle_setting') ?? false; - - // Env file setup for local development - String apiKey = Env.apiKey; - String awsAccessKey = Env.awsAccessKey; - String awsSecretKey = Env.awsSecretKey; - String awsRegion = Env.awsRegion; - - if (apiKey.isNotEmpty && preferences.getString('openai_api_key') == null) { - OpenAI.apiKey = apiKey; - await preferences.setString('openai_api_key', apiKey); - } else if (preferences.getString('openai_api_key') != null) { - OpenAI.apiKey = preferences.getString('openai_api_key')!; - } - - if (awsRegion.isNotEmpty && awsAccessKey.isNotEmpty && awsSecretKey.isNotEmpty) - { - await preferences.setString('aws_access_key', awsAccessKey); - await preferences.setString('aws_secret_key', awsSecretKey); - await preferences.setString('aws_region', awsRegion); - await preferences.setBool('is_aws_configured', true); - } - else - { - await preferences.setBool('is_aws_configured', false); - } - - if (Platform.isLinux || Platform.isWindows) { - sqfliteFfiInit(); - databaseFactory = databaseFactoryFfi; - } - - await dbHelper.database; - - if (preferences.getString('openai_api_key') == null || preferences.getBool('is_aws_configured') == null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - // Only show this dialog on first run - if (apiKey.isEmpty) { - showDialog( - context: navigatorKey.currentContext!, - builder: (BuildContext context) { - return AlertDialog( - title: Text('API Keys Required'), - content: Text('Please add valid API keys for OpenAI and AWS via the Settings menu.'), - actions: [ - TextButton( - child: Text('OK'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ); - } - }); - } - - // Run the app and initialize ThemeProvider - runApp( - ChangeNotifierProvider( - create: (_) => ThemeProvider()..toggleTheme(isDarkMode), // Initialize with saved theme - child: const MyApp(), - ), - ); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - final themeProvider = Provider.of(context); - - return MaterialApp( - navigatorKey: navigatorKey, - debugShowCheckedModeBanner: false, - title: 'Yappy', - theme: themeProvider.themeData, // Apply theme based on provider - darkTheme: ThemeProvider.darkTheme, // Provide dark theme separately - themeMode: themeProvider.isDarkMode ? ThemeMode.dark : ThemeMode.light, // Set theme mode based on isDarkMode - home: HomePage(), - builder: (context, child) { - // Wrap every screen with ToastWidget - return ToastWidget(child: child ?? Container()); - }, - ); - } -} diff --git a/team_b/yappy/lib/mechanic.dart b/team_b/yappy/lib/mechanic.dart deleted file mode 100644 index 81d0b3d7..00000000 --- a/team_b/yappy/lib/mechanic.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter/material.dart'; -import 'audiowave_widget.dart'; -import 'industry_menu.dart'; -import 'tool_bar.dart'; -import 'transcription_box.dart'; -import 'services/speech_state.dart'; -import 'services/model_manager.dart'; -import 'search_bar_widget.dart'; - -void main() { - runApp(MechanicalAidApp()); -} - -class MechanicalAidApp extends StatelessWidget { - const MechanicalAidApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: MechanicalAidPage(), - ); - } -} -//Creates a page for the Mechanical Aid industry -//The page will contain the industry menu and the transcription box -class MechanicalAidPage extends StatelessWidget { - MechanicalAidPage({super.key}); - final speechState = SpeechState(); - final modelManager = ModelManager(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PreferredSize( - preferredSize: Size.fromHeight(100), - child: ToolBar(), - ), - drawer: HamburgerDrawer(), - body: ListenableBuilder( - listenable: speechState, - builder: (context, child) { - return SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: SearchBarWidget(industry: "Vehicle Maintenance"), - ), - - IndustryMenu( - title: "Vehicle Maintenance", - icon: Icons.directions_car, - speechState: speechState, - modelManager: modelManager, - ), - - Padding( - padding: const EdgeInsets.all(16.0), - child: Column(children: [ - AudiowaveWidget(speechState: speechState), - TranscriptionBox( - controller: speechState.controller, - ), - ],) - - ), - ], - ) - ); - } - ), - ); - } -} diff --git a/team_b/yappy/lib/medical_doctor.dart b/team_b/yappy/lib/medical_doctor.dart deleted file mode 100644 index 0c035e5b..00000000 --- a/team_b/yappy/lib/medical_doctor.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; -import 'audiowave_widget.dart'; -import 'industry_menu.dart'; -import 'tool_bar.dart'; -import 'transcription_box.dart'; -import 'services/speech_state.dart'; -import 'services/model_manager.dart'; -import 'search_bar_widget.dart'; - - -class MedicalDoctorApp extends StatelessWidget { - const MedicalDoctorApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: MedicalDoctorPage(), - ); - } -} -//Creates a page for the Medical Doctor industry -//The page will contain the industry menu and the transcription box -class MedicalDoctorPage extends StatelessWidget { - MedicalDoctorPage({super.key}); - final speechState = SpeechState(); - final modelManager = ModelManager(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PreferredSize( - preferredSize: Size.fromHeight(100), - child: ToolBar() - ), - drawer: HamburgerDrawer(), - body: ListenableBuilder( - listenable: speechState, - builder: (context, child) { - return SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: SearchBarWidget(industry: "Medical Doctor"), - ), - IndustryMenu( - title: "Medical Doctor", - icon: Icons.medical_services, - speechState: speechState, - modelManager: modelManager, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column(children: [ - AudiowaveWidget(speechState: speechState), - TranscriptionBox( - controller: speechState.controller, - ), - ],) - - ), - ], - ) - ); - } - ), - ); - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/medical_patient.dart b/team_b/yappy/lib/medical_patient.dart deleted file mode 100644 index 52b4a970..00000000 --- a/team_b/yappy/lib/medical_patient.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; -import 'audiowave_widget.dart'; -import 'tool_bar.dart'; -import 'industry_menu.dart'; -import 'transcription_box.dart'; -import 'services/speech_state.dart'; -import 'services/model_manager.dart'; -import 'search_bar_widget.dart'; - - -class MedicalPatientApp extends StatelessWidget { - const MedicalPatientApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: MedicalPatientPage(), - ); - } -} -//Creates a page for the Medical Patient industry -//The page will contain the industry menu and the transcription box -class MedicalPatientPage extends StatelessWidget { - MedicalPatientPage({super.key}); - final speechState = SpeechState(); - final modelManager = ModelManager(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PreferredSize( - preferredSize: Size.fromHeight(100), - child: ToolBar() - ), - drawer: HamburgerDrawer(), - body: ListenableBuilder( - listenable: speechState, - builder: (context, child) { - return SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: SearchBarWidget(industry: "Medical Patient"), - ), - IndustryMenu( - title: "Medical Patient", - icon: Icons.local_pharmacy, - speechState: speechState, - modelManager: modelManager, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column(children: [ - AudiowaveWidget(speechState: speechState), - TranscriptionBox( - controller: speechState.controller, - ), - ],) - ), - ], - ) - ); - - } - ), - ); - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/restaurant.dart b/team_b/yappy/lib/restaurant.dart deleted file mode 100644 index 20df3c55..00000000 --- a/team_b/yappy/lib/restaurant.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'audiowave_widget.dart'; -import 'tool_bar.dart'; -import 'industry_menu.dart'; -import 'transcription_box.dart'; -import 'services/speech_state.dart'; -import 'services/model_manager.dart'; -import 'package:yappy/search_bar_widget.dart'; - -class RestaurantPage extends StatelessWidget { - RestaurantPage({super.key}); - final speechState = SpeechState(); - final modelManager = ModelManager(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PreferredSize( - preferredSize: Size.fromHeight(100), - child: ToolBar(), - ), - drawer: HamburgerDrawer(), - body: ListenableBuilder( - listenable: speechState, - builder: (context, child) { - return SingleChildScrollView( - child: Column( - children: [ - // Padding the search bar and using a smaller height - Padding( - padding: const EdgeInsets.all(8.0), - child: SearchBarWidget(industry: "Restaurant"), - ), - // The rest of the widgets below the search bar - IndustryMenu( - title: "Restaurant", - icon: Icons.restaurant_menu, - speechState: speechState, - modelManager: modelManager, - ), - - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - AudiowaveWidget(speechState: speechState), - TranscriptionBox( - controller: speechState.controller, - ), - ], - ), - ), - ], - ), - ); - }, - ), - ); - } -} - diff --git a/team_b/yappy/lib/search_bar_widget.dart b/team_b/yappy/lib/search_bar_widget.dart deleted file mode 100644 index 4df852dd..00000000 --- a/team_b/yappy/lib/search_bar_widget.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:yappy/services/database_helper.dart'; - -class SearchBarWidget extends StatefulWidget { - final String industry; - const SearchBarWidget({super.key, required this.industry}); - - @override - State createState() => _SearchBarWidgetState(); -} - -class _SearchBarWidgetState extends State { - late final SearchController _searchController; - DatabaseHelper dbHelp = DatabaseHelper(); - - @override - void initState() { - super.initState(); - _searchController = SearchController(); - } - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - - Future> _fetchSuggestions(String query, String industry) async { - List results = await dbHelp.searchTranscripts(query, industry); - - return results.map((item) { - return ListTile( - title: Text(item), - onTap: () { - _showDetailsDialog(item); - }, - ); - }).toList(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(left: 4.0, right: 4.0), - child: SearchAnchor( - searchController: _searchController, - suggestionsBuilder: (BuildContext context, SearchController controller) async { - return _fetchSuggestions(controller.text, widget.industry); - }, - builder: (BuildContext context, SearchController controller) { - return SearchBar( - controller: _searchController, - padding: const WidgetStatePropertyAll( - EdgeInsets.symmetric(horizontal: 2.0), - ), - onTap: () { - controller.openView(); - }, - onChanged: (_) { - controller.openView(); - }, - leading: const Icon(Icons.search), - ); - }, - ), - ); - } - - // Opens a pop-up window when users clicks on a specific search result - void _showDetailsDialog(String entry) async { - Map? transcriptDetails = await dbHelp.getTranscriptDetails(entry); - - if (transcriptDetails == null || !mounted) { - return; - } - - // Extract the results from the database query - String text = transcriptDetails['text'] ?? "No text available."; - String timestamp = transcriptDetails['timestamp'] ?? "No timestamp available."; - - // Shows the information extracted from the query - if (mounted) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text("Transcript Details"), - content: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Timestamp: $timestamp"), - SizedBox(height: 15.0), - Text("Transcript Text: $text"), - ], - ), - ), - actions: [ - TextButton( - child: const Text('Close'), - onPressed: () { - Navigator.of(context).pop(); - }, - ) - ], - ); - }, - ); - } - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/services/client.dart b/team_b/yappy/lib/services/client.dart deleted file mode 100644 index 3b677e2f..00000000 --- a/team_b/yappy/lib/services/client.dart +++ /dev/null @@ -1,231 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:aws_common/aws_common.dart'; -import 'package:aws_signature_v4/aws_signature_v4.dart'; -import 'package:http2/http2.dart'; - -import 'event_stream/message.dart'; -import 'event_stream/message_signer.dart'; -import 'event_stream/stream_codec.dart'; -import 'exceptions.dart'; -import 'models.dart'; -import 'protocol.dart'; - -/// A client for the Amazon Transcribe Streaming API. -final class TranscribeStreamingClient { - /// Creates a [TranscribeStreamingClient] with a given [region] and - /// [credentialsProvider]. - const TranscribeStreamingClient({ - required this.region, - required this.credentialsProvider, - }); - - /// Specifies the AWS region to use. - final String region; - - /// Specifies the credentials provider to use. - final AWSCredentialsProvider credentialsProvider; - - /// Starts a HTTP/2 stream where audio is streamed to Amazon Transcribe - /// and the [TranscriptEvent]s are streamed to your application. - Future< - ( - StartStreamTranscriptionResponse, - StreamSink, - Stream, - )> startStreamTranscription( - StartStreamTranscriptionRequest request, - ) async { - final (response, audioStreamSink, eventStreamMessages) = - await send(request); - - return ( - StartStreamTranscriptionResponse.fromHeaders(response.headers), - audioStreamSink, - eventStreamMessages.transform(StreamTransformer.fromHandlers( - handleData: - (EventStreamMessage event, EventSink sink) { - sink.add(TranscriptEvent.fromJson(utf8.decode(event.payload))); - }, - )), - ); - } - - /// Starts a HTTP/2 stream where audio is streamed to Amazon Transcribe - /// and the raw [EventStreamMessage]s are streamed to your application. - Future< - ( - AWSHttpResponse, - StreamSink, - Stream, - )> send( - TranscribeStreamingRequest request, - ) async { - final uri = - Uri.https('transcribestreaming.$region.amazonaws.com', request.path); - - final socket = await SecureSocket.connect( - uri.host, - 443, - supportedProtocols: ['h2'], - ); - - final connection = ClientTransportConnection.viaSocket(socket); - - final awsHttpRequest = AWSHttpRequest( - method: AWSHttpMethod.post, - uri: uri, - headers: { - AWSHeaders.target: request.target, - AWSHeaders.contentType: 'application/vnd.amazon.eventstream', - }, - body: null, - ); - - final credentialScope = AWSCredentialScope( - region: region, - service: AWSService.transcribeStreaming, - ); - - final signer = AWSSigV4Signer( - credentialsProvider: credentialsProvider, - ); - - final signedRequest = await signer.sign( - awsHttpRequest, - credentialScope: credentialScope, - ); - - final messageSigner = await EventStreamMessageSigner.create( - region: region, - signer: signer, - priorSignature: signedRequest.signature, - ); - - final headers = signedRequest.headers..addAll(request.toHeaders()); - - final clientTransportStream = connection.makeRequest([ - Header.ascii(':method', 'POST'), - Header.ascii(':path', signedRequest.path), - Header.ascii(':scheme', 'https'), - ...headers - .map((String key, String value) => - MapEntry(key, Header.ascii(key, value))) - .values, - ]); - - final responseHeadersCompleter = Completer>(); - final responseBodyCompleter = Completer>(); - late final bool hasResponseBody; - - final eventStreamMessageController = StreamController(); - final audioStreamController = StreamController(); - StreamSubscription? audioStreamSubscription; - - final incomingMessagesSubscription = - clientTransportStream.incomingMessages.listen( - (event) { - try { - if (event is HeadersStreamMessage) { - if (responseHeadersCompleter.isCompleted) { - throw const ProtocolException( - 'HeadersStreamMessage received after response headers were already completed.', - ); - } - - final headers = CaseInsensitiveMap({}); - headers.addEntries(event.headers.map((header) => - MapEntry(utf8.decode(header.name), utf8.decode(header.value)))); - hasResponseBody = int.parse(headers['content-length'] ?? '0') > 0; - responseHeadersCompleter.complete(headers); - } else if (event is DataStreamMessage) { - if (!responseHeadersCompleter.isCompleted) { - throw const ProtocolException( - 'DataStreamMessage received before response headers were completed.', - ); - } - - if (hasResponseBody && !responseBodyCompleter.isCompleted) { - responseBodyCompleter.complete(event.bytes); - return; - } - - final eventStreamMessage = - EventStreamCodec.decode(Uint8List.fromList(event.bytes)); - - final messageType = - eventStreamMessage.getHeaderValue(':message-type'); - - if (messageType == 'event') { - eventStreamMessageController.sink.add(eventStreamMessage); - } else if (messageType == 'exception') { - throw TranscribeStreamingServiceException.createFromResponse( - eventStreamMessage.getHeaderValue(':exception-type') ?? '', - eventStreamMessage.getHeaderValue(':content-type') ?? '', - eventStreamMessage.payload, - ); - } else { - throw UnexpectedMessageTypeException( - messageType, - eventStreamMessage, - ); - } - } - } catch (e) { - eventStreamMessageController.sink.addError(e); - } - }, - onDone: () async { - await audioStreamSubscription?.cancel(); - await audioStreamController.close(); - await eventStreamMessageController.close(); - await clientTransportStream.outgoingMessages.close(); - await connection.finish(); - }, - ); - - final responseHeaders = await responseHeadersCompleter.future; - final statusCode = int.parse(responseHeaders[':status']!); - List? responseBody; - - if (hasResponseBody) { - responseBody = await responseBodyCompleter.future; - } - - if (statusCode >= 400) { - await incomingMessagesSubscription.cancel(); - await clientTransportStream.outgoingMessages.close(); - await connection.finish(); - - throw TranscribeStreamingServiceException.createFromResponse( - (responseHeaders['x-amzn-errortype'] ?? statusCode.toString()) - .split(':') - .first, - responseHeaders['content-type'] ?? '', - responseBody, - ); - } - - audioStreamSubscription = audioStreamController.stream - .transform(AudioDataChunker(request.chunkSize)) - .transform(const AudioEventEncoder()) - .transform(const EventStreamEncoder()) - .transform(AudioMessageSigner(messageSigner)) - .transform(const EventStreamEncoder()) - .transform(const DataStreamMessageEncoder()) - .listen(clientTransportStream.outgoingMessages.add); - - return ( - AWSHttpResponse( - statusCode: statusCode, - headers: responseHeaders, - body: responseBody, - ), - audioStreamController.sink, - eventStreamMessageController.stream, - ); - } -} diff --git a/team_b/yappy/lib/services/database_helper.dart b/team_b/yappy/lib/services/database_helper.dart deleted file mode 100644 index b74b94b6..00000000 --- a/team_b/yappy/lib/services/database_helper.dart +++ /dev/null @@ -1,668 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:sqflite/sqflite.dart'; -import 'package:path/path.dart'; -import 'package:flutter/services.dart' show rootBundle; -import 'dart:io'; - -class DatabaseHelper { - static final DatabaseHelper _instance = DatabaseHelper._internal(); - static Database? _database; - - factory DatabaseHelper() { - return _instance; - } - - DatabaseHelper._internal(); - - Future get database async { - if (_database != null) return _database!; - _database = await _initDatabase(); - return _database!; - } - - Future _initDatabase() async { - String databasesPath = await getDatabasesPath(); - String path = join(databasesPath, 'yappy_database.db'); - - // Check if the database already exists - bool exists = await databaseExists(path); - - if (!exists) { - // Copy the database from assets if one doesn't exist on the device's filesystem - try { - ByteData data = await rootBundle.load('assets/yappy_database.db'); - List bytes = data.buffer.asUint8List(); - await File(path).writeAsBytes(bytes); - } catch (e) { - // Can create logging function in later task if wanted - if (kDebugMode) { - print('Error copying database: $e'); - } - } - } - - return await openDatabase( - path, - version: 1, - onCreate: _onCreate, - onOpen: (db) async { - // Check if the tables exist and create them if they don't - await _createTablesIfNotExists(db); - }, - ); - } - - Future _onCreate(Database db, int version) async { - // Create tables if needed - await _createTablesIfNotExists(db); - } - - Future _createTablesIfNotExists(Database db) async { - // Create Users table if it doesn't exist - await db.execute(''' - CREATE TABLE IF NOT EXISTS Users ( - user_id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT, - password TEXT, - role TEXT - ) - '''); - - // Create Vehicle table if it doesn't exist - await db.execute(''' - CREATE TABLE IF NOT EXISTS Vehicle ( - vehicle_id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - vehicle_make TEXT, - vehicle_model TEXT, - vehicle_year INTEGER, - FOREIGN KEY (user_id) REFERENCES Users(user_id) - ) - '''); - - - - // Create Transcript table if it doesn't exist - await db.execute(''' - CREATE TABLE IF NOT EXISTS Transcript ( - transcript_id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - transcript_text_data TEXT, - transcript_timestamp DATETIME, - transcript_ai_response TEXT, - transcript_document BLOB, - industry TEXT, - FOREIGN KEY (user_id) REFERENCES Users(user_id) - ) - '''); - - // Create VehicleMaintenance table if it doesn't exist - await db.execute(''' - CREATE TABLE IF NOT EXISTS VehicleMaintenance ( - maintenance_id INTEGER PRIMARY KEY AUTOINCREMENT, - transcript_id INTEGER, - user_id INTEGER, - vehicle_id INTEGER, - vehicle_diagnosis_description TEXT, - vehicle_required_parts TEXT, - FOREIGN KEY (transcript_id) REFERENCES Transcript(transcript_id), - FOREIGN KEY (user_id) REFERENCES Users(user_id), - FOREIGN KEY (vehicle_id) REFERENCES Vehicle(vehicle_id) - ) - '''); - - // Create SpecialRequest table if it doesn't exist - await db.execute(''' - CREATE TABLE IF NOT EXISTS SpecialRequest ( - special_request_id INTEGER PRIMARY KEY AUTOINCREMENT, - special_request_description TEXT - ) - '''); - - // Create MenuItem table if it doesn't exist - await db.execute(''' - CREATE TABLE IF NOT EXISTS MenuItem ( - item_id INTEGER PRIMARY KEY AUTOINCREMENT, - special_request_id INTEGER, - item_name TEXT, - item_description TEXT, - item_price REAL, - seat_position INTEGER, - FOREIGN KEY (special_request_id) REFERENCES SpecialRequest(special_request_id) - ) - '''); - - // Create RestaurantOrder table if it doesn't exist - await db.execute(''' - CREATE TABLE IF NOT EXISTS RestaurantOrder ( - order_id INTEGER PRIMARY KEY AUTOINCREMENT, - transcript_id INTEGER, - order_status TEXT, - order_total_cost REAL, - FOREIGN KEY (transcript_id) REFERENCES Transcript(transcript_id) - ) - '''); - - // Create OrderItems table if it doesn't exist - await db.execute(''' - CREATE TABLE IF NOT EXISTS OrderItems ( - order_item_id INTEGER PRIMARY KEY AUTOINCREMENT, - order_id INTEGER, - item_id INTEGER, - order_item_qty INTEGER, - FOREIGN KEY (order_id) REFERENCES RestaurantOrder(order_id), - FOREIGN KEY (item_id) REFERENCES MenuItem(item_id) - ) - '''); - - // Create Patient table if it doesn't exist - await db.execute(''' - CREATE TABLE IF NOT EXISTS Patient ( - patient_id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - patient_first_name TEXT, - patient_last_name TEXT, - patient_phone TEXT, - FOREIGN KEY (user_id) REFERENCES Users(user_id) - ) - '''); - - // Create DoctorVisit table if it doesn't exist - await db.execute(''' - CREATE TABLE IF NOT EXISTS DoctorVisit ( - visit_id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - transcript_id INTEGER, - patient_id INTEGER, - doctor_visit_symptoms TEXT, - doctor_visit_diagnosis TEXT, - FOREIGN KEY (user_id) REFERENCES Users(user_id), - FOREIGN KEY (transcript_id) REFERENCES Transcript(transcript_id), - FOREIGN KEY (patient_id) REFERENCES Patient(patient_id) - ) - '''); - } - - // Users table methods - Future>> getUsers() async { - final db = await database; - return await db.query('Users'); - } - - Future insertUser(Map user) async { - final db = await database; - return await db.insert('Users', user); - } - - Future updateUser(Map user) async { - final db = await database; - int id = user['user_id']; - return await db.update('Users', user, where: 'user_id = ?', whereArgs: [id]); - } - - Future deleteUser(int id) async { - final db = await database; - return await db.delete('Users', where: 'user_id = ?', whereArgs: [id]); - } - - // Vehicle table methods - Future insertVehicle(Map vehicle) async { - final db = await database; - return await db.insert('Vehicle', vehicle); - } - - Future updateVehicle(Map vehicle) async { - final db = await database; - int id = vehicle['vehicle_id']; - return await db.update('Vehicle', vehicle, where: 'vehicle_id = ?', whereArgs: [id]); - } - - Future deleteVehicle(int id) async { - final db = await database; - return await db.delete('Vehicle', where: 'vehicle_id = ?', whereArgs: [id]); - } - - // VehicleMaintenance table methods - Future insertVehicleMaintenance(Map maintenance) async { - final db = await database; - return await db.insert('VehicleMaintenance', maintenance); - } - - Future updateVehicleMaintenance(Map maintenance) async { - final db = await database; - int id = maintenance['maintenance_id']; - return await db.update('VehicleMaintenance', maintenance, where: 'maintenance_id = ?', whereArgs: [id]); - } - - Future deleteVehicleMaintenance(int id) async { - final db = await database; - return await db.delete('VehicleMaintenance', where: 'maintenance_id = ?', whereArgs: [id]); - } - - //Transcript table methods - Future insertTranscript(Map transcript) async { - final db = await database; - return await db.insert('Transcript', transcript); - } - - Future updateTranscript(Map transcript) async { - final db = await database; - int id = transcript['transcript_id']; - return await db.update( - 'Transcript', - transcript, - where: 'transcript_id = ?', - whereArgs: [id], - ); - } - - Future deleteTranscript(int id) async { - final db = await database; - return await db.delete('Transcript', where: 'transcript_id = ?', whereArgs: [id]); - } - - // Retrieve all transcripts - Future>> getAllTranscripts() async { - final db = await database; - return await db.query( - 'Transcript', - orderBy: 'transcript_timestamp DESC', - ); - } - - // Retrieve all transcripts by industry - Future>> getAllTranscriptsByIndustry(String industry) async { - final db = await database; - return await db.query( - 'Transcript', - where: 'industry = ?', - whereArgs: [industry], - orderBy: 'transcript_timestamp DESC', - ); - } - - // Retrieve a specific transcript by ID - Future?> getTranscriptById(int transcriptId) async { - final db = await database; - List> results = await db.query( - 'Transcript', - where: 'transcript_id = ?', - whereArgs: [transcriptId], - ); - if (results.isNotEmpty) { - return results.first; - } - return null; - } - - // RestaurantOrder table methods - Future insertOrder(Map order) async { - final db = await database; - return await db.insert('RestaurantOrder', order); - } - - Future updateOrder(Map order) async { - final db = await database; - int id = order['order_id']; - return await db.update('RestaurantOrder', order, where: 'order_id = ?', whereArgs: [id]); - } - - Future deleteOrder(int id) async { - final db = await database; - return await db.delete('RestaurantOrder', where: 'order_id = ?', whereArgs: [id]); - } - - // OrderItems table methods - Future insertOrderItem(Map orderItem) async { - final db = await database; - return await db.insert('OrderItems', orderItem); - } - - Future updateOrderItem(Map orderItem) async { - final db = await database; - int id = orderItem['order_item_id']; - return await db.update('OrderItems', orderItem, where: 'order_item_id = ?', whereArgs: [id]); - } - - Future deleteOrderItem(int id) async { - final db = await database; - return await db.delete('OrderItems', where: 'order_item_id = ?', whereArgs: [id]); - } - - // MenuItem table methods - Future>> getMenuItems() async { - final db = await database; - return await db.query('MenuItem'); - } - - Future insertMenuItem(Map menuItem) async { - final db = await database; - return await db.insert('MenuItem', menuItem); - } - - Future updateMenuItem(Map menuItem) async { - final db = await database; - int id = menuItem['item_id']; - return await db.update('MenuItem', menuItem, where: 'item_id = ?', whereArgs: [id]); - } - - Future deleteMenuItem(int id) async { - final db = await database; - return await db.delete('MenuItem', where: 'item_id = ?', whereArgs: [id]); - } - - // SpecialRequest table methods - Future insertSpecialRequest(Map specialRequest) async { - final db = await database; - return await db.insert('SpecialRequest', specialRequest); - } - - Future updateSpecialRequest(Map specialRequest) async { - final db = await database; - int id = specialRequest['special_request_id']; - return await db.update('SpecialRequest', specialRequest, where: 'special_request_id = ?', whereArgs: [id]); - } - - Future deleteSpecialRequest(int id) async { - final db = await database; - return await db.delete('SpecialRequest', where: 'special_request_id = ?', whereArgs: [id]); - } - - // DoctorVisit table methods - Future>> getDoctorVisits() async { - final db = await database; - return await db.query('DoctorVisit'); - } - - Future insertDoctorVisit(Map doctorVisit) async { - final db = await database; - return await db.insert('DoctorVisit', doctorVisit); - } - - Future updateDoctorVisit(Map doctorVisit) async { - final db = await database; - int id = doctorVisit['visit_id']; - return await db.update('DoctorVisit', doctorVisit, where: 'visit_id = ?', whereArgs: [id]); - } - - Future deleteDoctorVisit(int id) async { - final db = await database; - return await db.delete('DoctorVisit', where: 'visit_id = ?', whereArgs: [id]); - } - - // Patient table methods - Future insertPatient(Map patient) async { - final db = await database; - return await db.insert('Patient', patient); - } - - Future updatePatient(Map patient) async { - final db = await database; - int id = patient['patient_id']; - return await db.update('Patient', patient, where: 'patient_id = ?', whereArgs: [id]); - } - - Future deletePatient(int id) async { - final db = await database; - return await db.delete('Patient', where: 'patient_id = ?', whereArgs: [id]); - } - - // Specific use-case Query functions - - // Given DoctorVisit visit_id, get doctor_visit_symptoms and doctor_visit_diagnosis - Future?> getDoctorVisitById(int visitId) async { - final db = await database; - List> results = await db.query( - 'DoctorVisit', - columns: ['doctor_visit_symptoms', 'doctor_visit_diagnosis'], - where: 'visit_id = ?', - whereArgs: [visitId], - ); - if (results.isNotEmpty) { - return results.first; - } - return null; - } - - // Given DoctorVisit patient_id, get doctor_visit_symptoms and doctor_visit_diagnosis - Future>> getDoctorVisitsByPatientId(int patientId) async { - final db = await database; - return await db.query( - 'DoctorVisit', - columns: ['doctor_visit_symptoms', 'doctor_visit_diagnosis'], - where: 'patient_id = ?', - whereArgs: [patientId], - ); - } - - // Given VehicleMaintenance maintenance_id, get vehicle_diagnosis_description and vehicle_required_parts - Future?> getVehicleMaintenanceById(int maintenanceId) async { - final db = await database; - List> results = await db.query( - 'VehicleMaintenance', - columns: ['vehicle_diagnosis_description', 'vehicle_required_parts'], - where: 'maintenance_id = ?', - whereArgs: [maintenanceId], - ); - if (results.isNotEmpty) { - return results.first; - } - return null; - } - - // Given Vehicle vehicle_id, get vehicle_make, vehicle_model, and vehicle_year - Future?> getVehicleById(int vehicleId) async { - final db = await database; - List> results = await db.query( - 'Vehicle', - columns: ['vehicle_make', 'vehicle_model', 'vehicle_year'], - where: 'vehicle_id = ?', - whereArgs: [vehicleId], - ); - if (results.isNotEmpty) { - return results.first; - } - return null; - } - - // Given Vehicle user_id, get all vehicle_id with given user_id - Future> getVehicleIdsByUserId(int userId) async { - final db = await database; - List> results = await db.query( - 'Vehicle', - columns: ['vehicle_id'], - where: 'user_id = ?', - whereArgs: [userId], - ); - return results.map((result) => result['vehicle_id'] as int).toList(); - } - // Get all transcript_id with given user_id - Future> getTranscriptIdsByUserId(int userId) async { - final db = await database; - List> results = await db.query( - 'Transcript', - columns: ['transcript_id'], - where: 'user_id = ?', - whereArgs: [userId], - ); - return results.map((result) => result['transcript_id'] as int).toList(); - } - - // Get all maintenance_id with given user_id - Future> getMaintenanceIdsByUserId(int userId) async { - final db = await database; - List> results = await db.query( - 'VehicleMaintenance', - columns: ['maintenance_id'], - where: 'user_id = ?', - whereArgs: [userId], - ); - return results.map((result) => result['maintenance_id'] as int).toList(); - } - - // Get all item_id from order items with matching given order_id - Future> getItemIdsByOrderId(int orderId) async { - final db = await database; - List> results = await db.query( - 'OrderItems', - columns: ['item_id'], - where: 'order_id = ?', - whereArgs: [orderId], - ); - return results.map((result) => result['item_id'] as int).toList(); - } - - saveTranscript({required int userId, required int transcriptId, required String text, - required String industry,}) async { - Map transcript = { - 'user_id': userId, - 'transcript_id': transcriptId, - 'transcript_text_data': text, - 'transcript_timestamp': DateTime.now().toIso8601String(), - 'transcript_ai_response': '', - 'industry': industry, - }; - await insertTranscript(transcript); - } - - saveTranscriptAiResponse({required int userId, required int transcriptId, - required String text, required String aiResponse, required String industry}) async { - // Save the new transcript to the database using the provided information - Map transcript = { - 'user_id': userId, - 'transcript_id': transcriptId, - 'transcript_text_data': text, - 'transcript_timestamp': DateTime.now().toIso8601String(), - 'transcript_ai_response': aiResponse, - 'industry': industry - }; - await updateTranscript(transcript); - } - - // Search method that will query the transcript information in the database and return - // search results based on the information found. -Future> searchTranscripts(String query, String industry) async { - final db = await database; - - // Compares the query with the data within the transcripts and returns the entries. - final List> results = await db.query( - 'Transcript', - where: '(LOWER(transcript_text_data) LIKE LOWER(?) OR LOWER(transcript_ai_response) LIKE LOWER(?)) AND industry = ?', - whereArgs: ['%$query%', '%$query%', industry], - ); - - return results.map((row) => row['transcript_text_data'] as String).toList(); -} - -Future?> getTranscriptDetails(String entry) async { - final db = await database; - // Select the information i want to display from database - var result = await db.rawQuery( - "SELECT transcript_text_data, transcript_timestamp, transcript_ai_response FROM Transcript WHERE transcript_text_data = ? OR transcript_ai_response = ?", - [entry, entry] - ); - - // Grabs the results and return them - if (result.isNotEmpty) { - return { - 'text': result.first['transcript_text_data'] as String, - 'timestamp': result.first['transcript_timestamp'] as String, - 'ai_response': result.first['transcript_ai_response'] as String, - }; - } - return null; - } - - // Method to insert a document into the Transcript table - Future insertDocument(int transcriptId, String fileName, List fileBytes) async { - final db = await database; - Map document = { - 'transcript_id': transcriptId, - 'transcript_document': fileBytes, - }; - return await db.update( - 'Transcript', - document, - where: 'transcript_id = ?', - whereArgs: [transcriptId], - ); - } - - // Method to get transcript text data and industry by transcriptId - Future?> getTranscriptTextDataAndIndustryById(int transcriptId) async { - final db = await database; - List> results = await db.query( - 'Transcript', - columns: ['transcript_text_data', 'industry'], - where: 'transcript_id = ?', - whereArgs: [transcriptId], - ); - if (results.isNotEmpty) { - return { - 'transcript_text_data': results.first['transcript_text_data'] as String, - 'industry': results.first['industry'] as String, - }; - } - return null; - } - - // Method to get document (BLOB) and industry by transcriptId - Future?> getDocumentAndIndustryById(int transcriptId) async { - final db = await database; - List> results = await db.query( - 'Transcript', - columns: ['transcript_document', 'industry'], - where: 'transcript_id = ?', - whereArgs: [transcriptId], - ); - if (results.isNotEmpty) { - return { - 'transcript_document': results.first['transcript_document'], - 'industry': results.first['industry'] as String, - }; - } - return null; - } - - // Method to insert AI response into the Transcript table by transcriptId - Future insertAiResponse(int transcriptId, String aiResponse) async { - final db = await database; - Map values = { - 'transcript_ai_response': aiResponse, - }; - return await db.update( - 'Transcript', - values, - where: 'transcript_id = ?', - whereArgs: [transcriptId], - ); - } - - Future updateTranscriptDocument(int transcriptId, List documentBytes) async { - final db = await database; - int count = await db.update( - 'Transcript', - {'transcript_document': documentBytes}, - where: 'transcript_id = ?', - whereArgs: [transcriptId], - ); - return count > 0; - } -} - - // Commented out this method for future use - /* getTranscriptCountForDate(String date) { - // Get the number of transcripts for a given date - Future getTranscriptCountForDate(String date) async { - final db = await database; - List> results = await db.rawQuery( - 'SELECT COUNT(*) as count FROM Transcript WHERE DATE(transcript_timestamp) = ?', - [date], - ); - if (results.isNotEmpty) { - return results.first['count'] as int; - } - return 0; - } - } - }*/ diff --git a/team_b/yappy/lib/services/event_stream/exceptions.dart b/team_b/yappy/lib/services/event_stream/exceptions.dart deleted file mode 100644 index 3c3a8205..00000000 --- a/team_b/yappy/lib/services/event_stream/exceptions.dart +++ /dev/null @@ -1,19 +0,0 @@ -import '../exceptions.dart'; - -/// The event stream message coding/decoding exception. -abstract class EventStreamException extends TranscribeStreamingException { - /// Creates a [EventStreamException] with a given message. - const EventStreamException(super.message); -} - -/// The event stream message cannot be decoded. -final class EventStreamDecodeException extends EventStreamException { - /// Creates a [EventStreamDecodeException] with a given message. - const EventStreamDecodeException(super.message); -} - -/// The event stream message header cannot be decoded. -final class EventStreamHeaderDecodeException extends EventStreamException { - /// Creates a [EventStreamHeaderDecodeException] with a given message. - const EventStreamHeaderDecodeException(super.message); -} diff --git a/team_b/yappy/lib/services/event_stream/header.dart b/team_b/yappy/lib/services/event_stream/header.dart deleted file mode 100644 index b35cb38e..00000000 --- a/team_b/yappy/lib/services/event_stream/header.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:typed_data'; - -/// Base class for all Event Stream message headers. -sealed class EventStreamHeader { - /// Creates an [EventStreamHeader] with a given name and a value. - const EventStreamHeader(this.name, this.value); - - /// The header name. - final String name; - - /// The header value. - final T value; - - /// A string representation of this object. - @override - String toString() => '$runtimeType(name: $name, value: $value)'; -} - -/// [EventStreamHeader] with a boolean value. -final class EventStreamBoolHeader extends EventStreamHeader { - /// Creates an [EventStreamBoolHeader] with a given name and a boolean value. - const EventStreamBoolHeader(super.name, super.value); -} - -/// [EventStreamHeader] with a byte value. -final class EventStreamByteHeader extends EventStreamHeader { - /// Creates an [EventStreamByteHeader] with a given name and a byte value. - const EventStreamByteHeader(super.name, super.value); -} - -/// [EventStreamHeader] with a short value. -final class EventStreamShortHeader extends EventStreamHeader { - /// Creates an [EventStreamShortHeader] with a given name and a short value. - const EventStreamShortHeader(super.name, super.value); -} - -/// [EventStreamHeader] with an integer value. -final class EventStreamIntegerHeader extends EventStreamHeader { - /// Creates an [EventStreamIntegerHeader] with a given name and an integer - const EventStreamIntegerHeader(super.name, super.value); -} - -/// [EventStreamHeader] with a long value. -final class EventStreamLongHeader extends EventStreamHeader { - /// Creates an [EventStreamLongHeader] with a given name and a long value. - const EventStreamLongHeader(super.name, super.value); -} - -/// [EventStreamHeader] with a byte array value. -final class EventStreamByteArrayHeader extends EventStreamHeader { - /// Creates an [EventStreamByteArrayHeader] with a given name and a byte array - /// value. - const EventStreamByteArrayHeader(super.name, super.value); -} - -/// [EventStreamHeader] with a string value. -final class EventStreamStringHeader extends EventStreamHeader { - /// Creates an [EventStreamStringHeader] with a given name and a string value. - const EventStreamStringHeader(super.name, super.value); -} - -/// [EventStreamHeader] with a timestamp value. -final class EventStreamTimestampHeader extends EventStreamHeader { - /// Creates an [EventStreamTimestampHeader] with a given name and a timestamp - /// value. - const EventStreamTimestampHeader(super.name, super.value); -} - -/// [EventStreamHeader] with a UUID value. -final class EventStreamUuidHeader extends EventStreamHeader { - /// Creates an [EventStreamUuidHeader] with a given name and a UUID value. - const EventStreamUuidHeader(super.name, super.value); -} diff --git a/team_b/yappy/lib/services/event_stream/header_codec.dart b/team_b/yappy/lib/services/event_stream/header_codec.dart deleted file mode 100644 index e002017d..00000000 --- a/team_b/yappy/lib/services/event_stream/header_codec.dart +++ /dev/null @@ -1,207 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:uuid/uuid.dart'; - -// import 'exceptions.dart'; -import 'header.dart'; - -/// Possible types of [EventStreamHeader]. -enum EventStreamHeaderType { - boolTrue, - boolFalse, - byte, - short, - integer, - long, - byteArray, - string, - timestamp, - uuid, -} - -/// Codec for encoding and decoding [EventStreamHeader]. -final class EventStreamHeaderCodec { - /// Decodes [Uint8List] into [EventStreamHeader]. - static List decode(Uint8List bytes) { - final out = []; - int position = 0; - final headers = ByteData.view(bytes.buffer); - - while (position < headers.lengthInBytes) { - final nameLength = headers.getUint8(position++); - final name = utf8.decode(bytes.sublist(position, position + nameLength)); - position += nameLength; - - final type = EventStreamHeaderType.values[headers.getUint8(position++)]; - late EventStreamHeader header; - - switch (type) { - case EventStreamHeaderType.boolTrue: - header = EventStreamBoolHeader(name, true); - break; - case EventStreamHeaderType.boolFalse: - header = EventStreamBoolHeader(name, false); - break; - case EventStreamHeaderType.byte: - header = EventStreamByteHeader(name, headers.getInt8(position)); - position++; - break; - case EventStreamHeaderType.short: - header = EventStreamShortHeader( - name, - headers.getInt16(position, Endian.big), - ); - position += 2; - break; - case EventStreamHeaderType.integer: - header = EventStreamIntegerHeader( - name, - headers.getInt32(position, Endian.big), - ); - position += 4; - break; - case EventStreamHeaderType.long: - header = EventStreamLongHeader( - name, - headers.getInt64(position, Endian.big), - ); - position += 8; - break; - case EventStreamHeaderType.byteArray: - final binaryLength = headers.getUint16(position, Endian.big); - position += 2; - header = EventStreamByteArrayHeader( - name, - bytes.sublist(position, position + binaryLength), - ); - position += binaryLength; - break; - case EventStreamHeaderType.string: - final stringLength = headers.getUint16(position, Endian.big); - position += 2; - header = EventStreamStringHeader( - name, - utf8.decode(bytes.sublist(position, position + stringLength)), - ); - position += stringLength; - break; - case EventStreamHeaderType.timestamp: - header = EventStreamTimestampHeader( - name, - DateTime.fromMillisecondsSinceEpoch( - headers.getInt64(position, Endian.big), - ), - ); - position += 8; - break; - case EventStreamHeaderType.uuid: - header = EventStreamUuidHeader( - name, - Uuid.unparse(bytes.sublist(position, position + 16)), - ); - position += 16; - break; - // default: - // throw const EventStreamHeaderDecodeException( - // 'Unrecognized header type', - // ); - } - - out.add(header); - } - - return out; - } - - /// Encodes a list of [EventStreamHeader]s into [Uint8List]. - static Uint8List encodeHeaders(List headers) { - final bytes = []; - - for (final header in headers) { - bytes.addAll(EventStreamHeaderCodec.encodeHeader(header)); - } - - return Uint8List.fromList(bytes); - } - - /// Encodes a single [EventStreamHeader] into [Uint8List]. - static Uint8List encodeHeader(EventStreamHeader header) { - final bytes = []; - - final nameBytes = utf8.encode(header.name); - bytes.add(nameBytes.length); - bytes.addAll(nameBytes); - - switch (header) { - case EventStreamBoolHeader(:var value): - bytes.add( - value - ? EventStreamHeaderType.boolTrue.index - : EventStreamHeaderType.boolFalse.index, - ); - break; - case EventStreamByteHeader(:var value): - bytes.add(EventStreamHeaderType.byte.index); - bytes.add(value); - break; - case EventStreamShortHeader(:var value): - bytes.add(EventStreamHeaderType.short.index); - bytes.addAll(_encodeInt16(value)); - break; - case EventStreamIntegerHeader(:var value): - bytes.add(EventStreamHeaderType.integer.index); - bytes.addAll(_encodeInt32(value)); - break; - case EventStreamLongHeader(:var value): - bytes.add(EventStreamHeaderType.long.index); - bytes.addAll(_encodeInt64(value)); - break; - case EventStreamByteArrayHeader(:var value): - bytes.add(EventStreamHeaderType.byteArray.index); - bytes.addAll(_encodeUint16(value.lengthInBytes)); - bytes.addAll(value); - break; - case EventStreamStringHeader(:var value): - bytes.add(EventStreamHeaderType.string.index); - final valueBytes = utf8.encode(value); - bytes.addAll(_encodeUint16(valueBytes.length)); - bytes.addAll(valueBytes); - break; - case EventStreamTimestampHeader(:var value): - bytes.add(EventStreamHeaderType.timestamp.index); - bytes.addAll(_encodeInt64(value.millisecondsSinceEpoch)); - break; - case EventStreamUuidHeader(:var value): - bytes.add(EventStreamHeaderType.uuid.index); - bytes.addAll(Uuid.parse(value)); - break; - } - - return Uint8List.fromList(bytes); - } - - static Uint8List _encodeUint16(int value) { - final bytes = Uint8List(2); - ByteData.view(bytes.buffer).setUint16(0, value, Endian.big); - return bytes; - } - - static Uint8List _encodeInt16(int value) { - final bytes = Uint8List(2); - ByteData.view(bytes.buffer).setInt16(0, value, Endian.big); - return bytes; - } - - static Uint8List _encodeInt32(int value) { - final bytes = Uint8List(4); - ByteData.view(bytes.buffer).setInt32(0, value, Endian.big); - return bytes; - } - - static Uint8List _encodeInt64(int value) { - final bytes = Uint8List(8); - ByteData.view(bytes.buffer).setInt64(0, value, Endian.big); - return bytes; - } -} diff --git a/team_b/yappy/lib/services/event_stream/message.dart b/team_b/yappy/lib/services/event_stream/message.dart deleted file mode 100644 index 2004a624..00000000 --- a/team_b/yappy/lib/services/event_stream/message.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'dart:typed_data'; - -import 'header.dart'; - -/// An Event Stream message. -final class EventStreamMessage { - /// Creates an [EventStreamMessage] with a given headers and payload. - const EventStreamMessage({ - required this.headers, - required this.payload, - }); - - /// The message headers. - final List headers; - - /// The message payload. - final Uint8List payload; - - /// Returns the value of the header with the specified [name] or `null` - /// if there is no such header. - dynamic getHeaderValue(String name) { - try { - return headers.firstWhere((header) => header.name == name).value; - } catch (_) { - return null; - } - } - - /// A string representation of this object. - @override - String toString() => - 'EventStreamMessage(headers: $headers, payload: $payload)'; -} diff --git a/team_b/yappy/lib/services/event_stream/message_signer.dart b/team_b/yappy/lib/services/event_stream/message_signer.dart deleted file mode 100644 index 94135a24..00000000 --- a/team_b/yappy/lib/services/event_stream/message_signer.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'dart:typed_data'; - -import 'package:aws_common/aws_common.dart'; -import 'package:aws_signature_v4/aws_signature_v4.dart'; -import 'package:convert/convert.dart'; -import 'package:crypto/crypto.dart'; - -import 'header.dart'; -import 'header_codec.dart'; -import 'message.dart'; - -/// Signs [Uint8List] payload into [EventStreamMessage]. -final class EventStreamMessageSigner { - EventStreamMessageSigner._create( - this._region, - this._signer, - this._credentials, - this._priorSignature, - ); - - final String _region; - final AWSSigV4Signer _signer; - final AWSCredentials _credentials; - - String _priorSignature; - - /// Creates a new [EventStreamMessageSigner]. - static Future create({ - required String region, - required AWSSigV4Signer signer, - required String priorSignature, - }) async { - final credentials = await signer.credentialsProvider.retrieve(); - - return EventStreamMessageSigner._create( - region, - signer, - credentials, - priorSignature, - ); - } - - /// Signs [Uint8List] payload into [EventStreamMessage]. - EventStreamMessage sign(Uint8List payload) { - final credentialScope = AWSCredentialScope( - region: _region, - service: AWSService.transcribeStreaming, - ); - - final signingKey = _signer.algorithm.deriveSigningKey( - _credentials, - credentialScope, - ); - - final List messageHeaders = [ - EventStreamTimestampHeader(':date', credentialScope.dateTime.dateTime), - ]; - - final nonSignatureHeaders = - EventStreamHeaderCodec.encodeHeaders(messageHeaders); - - final sb = StringBuffer() - ..writeln('${_signer.algorithm}-PAYLOAD') - ..writeln(credentialScope.dateTime) - ..writeln(credentialScope) - ..writeln(_priorSignature) - ..writeln(_hexHash(nonSignatureHeaders)) - ..write(_hexHash(payload)); - final stringToSign = sb.toString(); - - final signature = _signer.algorithm.sign(stringToSign, signingKey); - - messageHeaders.add( - EventStreamByteArrayHeader( - ':chunk-signature', - Uint8List.fromList(hex.decode(signature)), - ), - ); - - _priorSignature = signature; - - return EventStreamMessage(headers: messageHeaders, payload: payload); - } - - String _hexHash(Uint8List payload) => - hex.encode(sha256.convert(payload).bytes); -} diff --git a/team_b/yappy/lib/services/event_stream/stream_codec.dart b/team_b/yappy/lib/services/event_stream/stream_codec.dart deleted file mode 100644 index 9543c5bc..00000000 --- a/team_b/yappy/lib/services/event_stream/stream_codec.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'dart:typed_data'; - -import 'package:archive/archive.dart'; - -import 'exceptions.dart'; -import 'header_codec.dart'; -import 'message.dart'; - -/// Codec for encoding and decoding [EventStreamMessage]. -final class EventStreamCodec { - static const preludeMemberLength = 4; - static const preludeLength = preludeMemberLength * 2; - static const checksumLength = 4; - static const minimumMessageLength = preludeLength + checksumLength * 2; - - /// Decodes [Uint8List] into [EventStreamMessage]. - static EventStreamMessage decode(Uint8List message) { - final byteLength = message.lengthInBytes; - - if (byteLength < minimumMessageLength) { - throw const EventStreamDecodeException( - 'Provided message is too short to accommodate event stream ' - 'message overhead', - ); - } - - final view = ByteData.view(message.buffer); - - final messageLength = view.getUint32(0, Endian.big); - - if (byteLength != messageLength) { - throw const EventStreamDecodeException( - 'Reported message length does not match received message length', - ); - } - - final headerLength = view.getUint32(preludeMemberLength, Endian.big); - final expectedPreludeChecksum = view.getUint32(preludeLength, Endian.big); - final expectedMessageChecksum = - view.getUint32(byteLength - checksumLength, Endian.big); - - final preludeChecksum = getCrc32(message.sublist(0, preludeLength)); - if (expectedPreludeChecksum != preludeChecksum) { - throw EventStreamDecodeException( - 'The prelude checksum specified in the message ' - '($expectedPreludeChecksum) does not match the calculated CRC32 ' - 'checksum ($preludeChecksum)', - ); - } - - final messageChecksum = - getCrc32(message.sublist(0, byteLength - checksumLength)); - if (expectedMessageChecksum != messageChecksum) { - throw EventStreamDecodeException( - 'The message checksum ($messageChecksum) did not match ' - 'the expected value of $expectedMessageChecksum', - ); - } - - final encodedHeaders = message.sublist(preludeLength + checksumLength, - preludeLength + checksumLength + headerLength); - final payload = message.sublist( - preludeLength + checksumLength + headerLength, - byteLength - checksumLength, - ); - - return EventStreamMessage( - headers: EventStreamHeaderCodec.decode(encodedHeaders), - payload: payload, - ); - } - - /// Encodes [EventStreamMessage] into [Uint8List]. - static Uint8List encode(EventStreamMessage message) { - final headers = EventStreamHeaderCodec.encodeHeaders(message.headers); - final length = headers.lengthInBytes + - message.payload.lengthInBytes + - minimumMessageLength; - - final out = Uint8List(length); - final view = - ByteData.view(out.buffer, out.offsetInBytes, out.lengthInBytes); - - view.setUint32(0, length, Endian.big); - view.setUint32(preludeMemberLength, headers.lengthInBytes, Endian.big); - view.setUint32( - preludeLength, - getCrc32(out.sublist(0, preludeLength)), - Endian.big, - ); - out.setRange( - preludeLength + checksumLength, - preludeLength + checksumLength + headers.lengthInBytes, - headers, - ); - out.setRange( - preludeLength + checksumLength + headers.lengthInBytes, - length - checksumLength, - message.payload, - ); - - view.setUint32( - length - checksumLength, - getCrc32(out.sublist(0, length - checksumLength)), - Endian.big, - ); - - return out; - } -} diff --git a/team_b/yappy/lib/services/exceptions.dart b/team_b/yappy/lib/services/exceptions.dart deleted file mode 100644 index ddd55dd7..00000000 --- a/team_b/yappy/lib/services/exceptions.dart +++ /dev/null @@ -1,203 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'event_stream/message.dart'; - -/// Base class for all exceptions generated by the library. -abstract class TranscribeStreamingException implements Exception { - /// Creates a [TranscribeStreamingException] with the given message. - const TranscribeStreamingException(this.message); - - /// The exception message. - final String message; - - /// A string representation of this object. - @override - String toString() => '$runtimeType: $message'; -} - -/// Base class for all exceptions generated by AWS Transcribe Streaming service. -abstract class TranscribeStreamingServiceException - extends TranscribeStreamingException { - /// Creates a [TranscribeStreamingServiceException] with the given exception - /// name and message. - const TranscribeStreamingServiceException(this.exceptionName, super.message); - - /// The exception name. - final String exceptionName; - - /// Creates an exception from AWS Transcribe Streaming service response. - factory TranscribeStreamingServiceException.createFromResponse( - String exceptionType, - String contentType, - List? body, - ) => - _createTranscribeStreamingServiceException( - exceptionType, - contentType, - body, - ); - - /// A string representation of this object. - @override - String toString() => '$exceptionName: $message'; -} - -/// Base class for all server-side exceptions generated by AWS Transcribe -/// Streaming service. -abstract class TranscribeStreamingServiceServerException - extends TranscribeStreamingServiceException { - /// Creates a [TranscribeStreamingServiceServerException] with the given - /// exception name and message. - const TranscribeStreamingServiceServerException(super.name, super.message); -} - -/// Base class for all client-side exceptions generated by AWS Transcribe -/// Streaming service. -abstract class TranscribeStreamingServiceClientException - extends TranscribeStreamingServiceException { - /// Creates a [TranscribeStreamingServiceClientException] with the given - /// exception name and message. - const TranscribeStreamingServiceClientException(super.name, super.message); -} - -/// One or more arguments of [StartStreamTranscriptionRequest] were invalid. -/// -/// For example, `mediaEncoding` or `languageCode` used not valid values. -/// Check the specified parameters and try your request again. -final class BadRequestException - extends TranscribeStreamingServiceClientException { - /// Creates a [BadRequestException] with the given message. - const BadRequestException(String message) : super(name, message); - - /// The exception name. - static const name = 'BadRequestException'; -} - -/// The provided request signature is not valid. -final class InvalidSignatureException - extends TranscribeStreamingServiceClientException { - /// Creates a [InvalidSignatureException] with the given message. - const InvalidSignatureException(String message) : super(name, message); - - /// The exception name. - static const name = 'InvalidSignatureException'; -} - -/// A new stream started with the same session ID. -/// -/// The current stream has been terminated. -final class ConflictException - extends TranscribeStreamingServiceClientException { - /// Creates a [ConflictException] with the given message. - const ConflictException(String message) : super(name, message); - - /// The exception name. - static const name = 'ConflictException'; -} - -/// A problem occurred while processing the audio. -/// -/// Amazon Transcribe terminated processing. -final class InternalFailureException - extends TranscribeStreamingServiceServerException { - /// Creates a [InternalFailureException] with the given message. - const InternalFailureException(String message) : super(name, message); - - /// The exception name. - static const name = 'InternalFailureException'; -} - -/// Your client has exceeded one of the Amazon Transcribe limits. -/// -/// This is typically the audio length limit. -/// Break your audio stream into smaller chunks and try your request again. -final class LimitExceededException - extends TranscribeStreamingServiceClientException { - /// Creates a [LimitExceededException] with the given message. - const LimitExceededException(String message) : super(name, message); - - /// The exception name. - static const name = 'LimitExceededException'; -} - -/// The service is currently unavailable. -/// -/// Try your request later. -final class ServiceUnavailableException - extends TranscribeStreamingServiceServerException { - /// Creates a [ServiceUnavailableException] with the given message. - const ServiceUnavailableException(String message) : super(name, message); - - /// The exception name. - static const name = 'ServiceUnavailableException'; -} - -/// The received exception has unexpected type. -final class UnexpectedExceptionTypeException - extends TranscribeStreamingServiceException { - /// Creates a [UnexpectedExceptionTypeException] with the given type - /// and message. - const UnexpectedExceptionTypeException(this.exceptionType, String message) - : super('UnexpectedExceptionTypeException', '$exceptionType: $message'); - - /// The exception type. - final String exceptionType; -} - -/// The received message has unexpected type. -final class UnexpectedMessageTypeException - extends TranscribeStreamingException { - /// Creates a [UnexpectedMessageTypeException] with the given message type - /// and Event Stream message. - const UnexpectedMessageTypeException( - this.messageType, - this.eventStreamMessage, - ) : super('$messageType: $eventStreamMessage'); - - /// The message type. - final dynamic messageType; - - /// The received message. - final EventStreamMessage eventStreamMessage; -} - -/// Data interchange protocol violation. -/// -/// For example, several header messages are received, or body message -/// is received before headers. -final class ProtocolException extends TranscribeStreamingException { - /// Creates a [ProtocolException] with the given message. - const ProtocolException(super.message); -} - -TranscribeStreamingServiceException _createTranscribeStreamingServiceException( - String exceptionType, - String contentType, - List? body, -) { - final responseBody = utf8.decode(body ?? Uint8List(0)); - String message = responseBody; - - if (responseBody.isNotEmpty && contentType.contains('json')) { - final jsonResponse = json.decode(responseBody) as Map; - - if (jsonResponse.containsKey('Message')) { - message = jsonResponse['Message']; - } else if (jsonResponse.containsKey('message')) { - message = jsonResponse['message']; - } - } - - return switch (exceptionType) { - BadRequestException.name || '400' => BadRequestException(message), - InvalidSignatureException.name => InvalidSignatureException(message), - ConflictException.name || '409' => ConflictException(message), - LimitExceededException.name || '429' => LimitExceededException(message), - InternalFailureException.name || '500' => InternalFailureException(message), - ServiceUnavailableException.name || - '503' => - ServiceUnavailableException(message), - _ => UnexpectedExceptionTypeException(exceptionType, message), - }; -} diff --git a/team_b/yappy/lib/services/file_handler.dart b/team_b/yappy/lib/services/file_handler.dart deleted file mode 100644 index 6f30dd95..00000000 --- a/team_b/yappy/lib/services/file_handler.dart +++ /dev/null @@ -1,155 +0,0 @@ -import 'dart:io'; -import 'package:flutter/services.dart' show rootBundle; -import 'package:path_provider/path_provider.dart'; -import 'package:path/path.dart'; -import 'package:sqflite/sqflite.dart'; -import 'package:yappy/services/database_helper.dart'; -import 'package:flutter/foundation.dart'; - -class FileHandler { - Future get localStoragePath async { - final directory = await getApplicationDocumentsDirectory(); - return directory.path; - } - - Future get databasePath async { - final databasesPath = await getDatabasesPath(); - return join(databasesPath, 'yappy_database.db'); - } - - Future addDocument(File file) async { - try { - final path = await localStoragePath; - final newFile = File(join(path, basename(file.path))); - await file.copy(newFile.path); - if (kDebugMode) { - print('File added to local storage: ${newFile.path}'); - } - } catch (e) { - if (kDebugMode) { - print('Error adding document: $e'); - } - } - } - - // Used to move a file the user uploads from local storage to the database - Future moveFileToDatabase(DatabaseHelper dbHelper, String fileName, int transcriptId) async { - try { - final path = await localStoragePath; - final file = File(join(path, fileName)); - if (await file.exists()) { - final bytes = await file.readAsBytes(); - await dbHelper.insertDocument(transcriptId, fileName, bytes); - - await file.delete(); - if (kDebugMode) { - print('File moved to database and deleted from local storage: $fileName'); - } - } else { - if (kDebugMode) { - print('File not found in local storage: $fileName'); - } - } - } catch (e) { - if (kDebugMode) { - print('Error moving file to database: $e'); - } - } - } - - // Method to save transcript text data to local storage as a text file - Future saveTranscriptTextToLocal(DatabaseHelper dbHelper, int transcriptId) async { - try { - final transcriptData = await dbHelper.getTranscriptTextDataAndIndustryById(transcriptId); - if (transcriptData != null) { - final textData = transcriptData['transcript_text_data']!; - final industry = transcriptData['industry']!; - final fileName = 'transcript_text_${transcriptId}_$industry.txt'; - final path = await localStoragePath; - final file = File(join(path, fileName)); - await file.writeAsString(textData); - if (kDebugMode) { - print('Transcript text data saved to local storage: $fileName'); - } - return fileName; - } else { - if (kDebugMode) { - print('No transcript data found for ID: $transcriptId'); - } - } - } catch (e) { - if (kDebugMode) { - print('Error saving transcript text data to local storage: $e'); - } - } - return ''; - } - - // Method to save document (BLOB) to local storage as a text file - Future saveDocumentToLocal(DatabaseHelper dbHelper, int transcriptId) async { - try { - final documentData = await dbHelper.getDocumentAndIndustryById(transcriptId); - if (documentData != null) { - final documentBytes = documentData['transcript_document'] as List?; - final industry = documentData['industry']!; - if (documentBytes != null) { - final fileName = 'transcript_document_${transcriptId}_$industry.txt'; - final path = await localStoragePath; - final file = File(join(path, fileName)); - await file.writeAsBytes(documentBytes); - if (kDebugMode) { - print('Document saved to local storage: $fileName'); - } - } else { - if (kDebugMode) { - print('No document found for ID: $transcriptId'); - } - } - } else { - if (kDebugMode) { - print('No document data found for ID: $transcriptId'); - } - } - } catch (e) { - if (kDebugMode) { - print('Error saving document to local storage: $e'); - } - } - } - - Future deleteFile(String fileName) async { - try { - final path = await localStoragePath; - final file = File(join(path, fileName)); - if (await file.exists()) { - await file.delete(); - if (kDebugMode) { - print('File deleted from local storage: $fileName'); - } - } else { - if (kDebugMode) { - print('File not found in local storage: $fileName'); - } - } - } catch (e) { - if (kDebugMode) { - print('Error deleting file: $e'); - } - } - } - - Future copyAssetToLocalStorage(String assetPath, String fileName) async { - try { - final byteData = await rootBundle.load(assetPath); - final file = File(join(await localStoragePath, fileName)); - await file.writeAsBytes(byteData.buffer.asUint8List()); - if (kDebugMode) { - print('File copied from assets to local storage: $fileName'); - } - } catch (e) { - if (kDebugMode) { - print('Error copying file from assets to local storage: $e'); - } - } - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/services/mechanic_api_module.dart b/team_b/yappy/lib/services/mechanic_api_module.dart deleted file mode 100644 index 3a677473..00000000 --- a/team_b/yappy/lib/services/mechanic_api_module.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'database_helper.dart'; - -class MechanicAPI { - final DatabaseHelper dbHelper = DatabaseHelper(); - - // Fetch the latest transcription for a given user. - // This function gets the transcript IDs for the user, - // then retrieves the text from the most recent transcript. - Future getLatestTranscription(int userId) async { - List transcriptIds = await dbHelper.getTranscriptIdsByUserId(userId); - if (transcriptIds.isEmpty) return null; - int latestTranscriptId = transcriptIds.last; - final db = await dbHelper.database; - List> result = await db.query( - 'Transcript', - columns: ['transcript_text_data'], - where: 'transcript_id = ?', - whereArgs: [latestTranscriptId], - ); - if (result.isNotEmpty) { - return result.first['transcript_text_data'] as String?; - } - return null; - } - - // Simulated extraction function. - // Since the NLP extraction and summarization are assumed to be completed already, - // thisfunction returns processed details based on the transcription content. - Map extractMechanicData(String transcription) { - if (transcription.contains("Honda")) { - return { - 'vehicle': 'Honda Civic 2015', - 'diagnosis': - 'Brake pads require inspection, possible wear; engine knocking suggests immediate engine checkup.', - 'parts': 'Brake pads, Engine oil' - }; - } else { - return { - 'vehicle': 'Unknown Vehicle', - 'diagnosis': 'No diagnostic details available', - 'parts': 'N/A' - }; - } - } - - // Store the processed mechanic service details into the VehicleMaintenance table. - // Note: The VehicleMaintenance table requires a vehicle_id. - // if the vehicle is not directly identified, a dummy value (e.g., 0) is used. - Future storeMechanicService( - int userId, int transcriptId, Map mechanicData) async { - int vehicleId = 1; // Ensure this is valid from DB - - await dbHelper.insertVehicleMaintenance({ - 'transcript_id': transcriptId, - 'user_id': userId, - 'vehicle_id': vehicleId, - 'vehicle_diagnosis_description': mechanicData['diagnosis'], - 'vehicle_required_parts': mechanicData['parts'] - }); - - debugPrint(" Mechanic service stored successfully!"); - } - - // Combines steps: Fetch transcription, extract data, and store the report. - // Returns a report containing the transcription and the processed mechanic data. - Future?> getLatestMechanicReport(int userId) async { - String? transcription = await getLatestTranscription(userId); - if (transcription == null) return null; - Map mechanicData = extractMechanicData(transcription); - List transcriptIds = await dbHelper.getTranscriptIdsByUserId(userId); - int latestTranscriptId = transcriptIds.last; - await storeMechanicService(userId, latestTranscriptId, mechanicData); - return { - 'transcription': transcription, - 'vehicle': mechanicData['vehicle'], - 'diagnosis': mechanicData['diagnosis'], - 'parts': mechanicData['parts'] - }; - } - - // Retrieve the service history for the mechanic API. - // This fetches all maintenance records associated with the user. - Future>> getMechanicServiceHistory( - int userId) async { - List maintenanceIds = await dbHelper.getMaintenanceIdsByUserId(userId); - final db = await dbHelper.database; - List> history = []; - for (int id in maintenanceIds) { - List> result = await db.query( - 'VehicleMaintenance', - where: 'maintenance_id = ?', - whereArgs: [id], - ); - if (result.isNotEmpty) { - history.add(result.first); - } - } - return history; - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/services/medical_api_module.dart b/team_b/yappy/lib/services/medical_api_module.dart deleted file mode 100644 index 642d6f32..00000000 --- a/team_b/yappy/lib/services/medical_api_module.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'database_helper.dart'; - -class MedicalAPI { - final DatabaseHelper dbHelper = DatabaseHelper(); - - /// 📌 Step 3.1: Retrieve the latest transcription of a patient's consultation. - Future getConsultationTranscription(int visitId) async { - Map? visitData = await dbHelper.getDoctorVisitById(visitId); - - // Extract the transcription text if available, otherwise return null - return visitData?['transcription_text'] as String? ?? "No Transcription Available"; -} - - /// 📌 Step 3.2: Retrieve symptoms & diagnosis already stored in the database. - Future?> getSymptomsAndDiagnosis(int patientId) async { - final records = await dbHelper.getDoctorVisitsByPatientId(patientId); - - if (records.isNotEmpty) { - final latestRecord = records.last; // Get the most recent visit entry - return { - 'symptoms': latestRecord['doctor_visit_symptoms'] ?? 'Unknown', - 'diagnosis': latestRecord['doctor_visit_diagnosis'] ?? 'Unknown', - }; - } - - return null; // No records found - } - - /// 📌 Step 4.1: Store the visit into the DoctorVisit table - Future storeDoctorVisit({ - required int userId, - required int patientId, - required int transcriptId, - required String symptoms, - required String diagnosis, - }) async { - final visitMap = { - 'user_id': userId, - 'transcript_id': transcriptId, - 'patient_id': patientId, - 'doctor_visit_symptoms': symptoms, - 'doctor_visit_diagnosis': diagnosis, - }; - - int visitId = await dbHelper.insertDoctorVisit(visitMap); - return visitId; - } - - /// 📌 Step 4.2: Ensure only authorized users can access records. - /// (For now, we're assuming a secure retrieval function is in place) - Future authorizeUser(int userId) async { - // Assume only doctors or the patient themselves can access - // You can implement role-based access checks here. - return true; // Placeholder for now - } - - /// 📌 Step 5.1: Retrieve ALL medical records for a given patient. - Future>> getPatientMedicalRecords( - int patientId) async { - return await dbHelper.getDoctorVisitsByPatientId(patientId); - } - - /// 📌 Step 5.2: Retrieve past consultations (Transcript + Diagnosis). - Future>> getPastConsultations(int patientId) async { - final medicalRecords = await getPatientMedicalRecords(patientId); - - // Ensure pastConsultations list is initialized - List> pastConsultations = []; - - for (var record in medicalRecords) { - int? transcriptId = - record['transcript_id'] as int?; // Ensure transcriptId is nullable - - // Safely retrieve transcript - String? transcriptText; - if (transcriptId != null) { - Map? transcriptData = await dbHelper.getTranscriptById(transcriptId); - - // Extract the actual transcript text if available - transcriptText = transcriptData?['transcript_text'] as String? ?? "No Transcript Available"; - } else { - transcriptText = "No Transcript Available"; // Fallback for null transcriptId - } - - // Add the record with the transcript to the consultations list - pastConsultations.add({ - ...record, - 'transcript_text': transcriptText, // Include transcript text in the result - }); - - } - - return pastConsultations; - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/services/model_manager.dart b/team_b/yappy/lib/services/model_manager.dart deleted file mode 100644 index c216c29a..00000000 --- a/team_b/yappy/lib/services/model_manager.dart +++ /dev/null @@ -1,578 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:isolate'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:http/http.dart' as http; -import 'package:archive/archive.dart'; -import 'package:path/path.dart' as path; -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'toast_service.dart'; - -// Data class for passing to isolate -class ExtractionData { - final List fileBytes; - final String modelDir; - final List outputMappings; - final SendPort sendPort; - - ExtractionData({ - required this.fileBytes, - required this.modelDir, - required this.outputMappings, - required this.sendPort, - }); -} - -// Class for mapping source files to destination names -class OutputFileMapping { - final String source; - final String destination; - - OutputFileMapping({ - required this.source, - required this.destination, - }); - - // Factory constructor from JSON - factory OutputFileMapping.fromJson(Map json) { - return OutputFileMapping( - source: json['source'], - destination: json['destination'], - ); - } -} - -class ModelInfo { - final String id; - final String name; - final String url; - final bool isCompressed; - final String type; - final String? modelType; - final List outputFiles; - final double size; // Size in MB - - ModelInfo({ - required this.id, - required this.name, - required this.url, - required this.isCompressed, - required this.type, - this.modelType, - required this.outputFiles, - required this.size, - }); - - // Factory constructor to create ModelInfo from JSON - factory ModelInfo.fromJson(Map json) { - final outputFilesJson = json['outputFiles'] as List; - - return ModelInfo( - id: json['id'], - name: json['name'], - url: json['url'], - isCompressed: json['isCompressed'], - type: json['type'], - modelType: json['modelType'], // Added modelType - outputFiles: outputFilesJson - .map((fileJson) => OutputFileMapping.fromJson(fileJson)) - .toList(), - size: json['size'], - ); - } - - String getFilename() { - return path.basename(url); - } -} - -class ModelManager { - // Base directory for storing models - late final Future _modelDirPath; - - // List to hold models loaded from config - List _models = []; - - // Config file path in assets - static const String _configAssetPath = 'assets/models_config.json'; - - final _toastService = ToastService(); - - ModelManager() { - _modelDirPath = _initModelDir(); - _loadModelsFromConfig(); - } - - // Initialize the models directory - Future _initModelDir() async { - final appDir = await getApplicationCacheDirectory(); - final modelDir = Directory('${appDir.path}/models'); - if (!await modelDir.exists()) { - await modelDir.create(recursive: true); - } - return modelDir.path; - } - - // Load models from the config file - Future _loadModelsFromConfig() async { - try { - // Load the JSON file from assets - final String jsonContent = await rootBundle.loadString(_configAssetPath); - final Map configData = json.decode(jsonContent); - - // Parse models - final List modelsJson = configData['models']; - _models = modelsJson.map((modelJson) => ModelInfo.fromJson(modelJson)).toList(); - - debugPrint('Loaded ${_models.length} models from config'); - } catch (e) { - debugPrint('Error loading models from config: $e'); - // Fallback to empty list - _models = []; - } - } - - // Reload models from config - Future reloadModelsConfig() async { - await _loadModelsFromConfig(); - } - - // Get the list of models (for UI display) - List get models => List.unmodifiable(_models); - - // Get model path by type and file type - Future getModelPath(String modelType, String fileName) async { - final modelDir = await _modelDirPath; - return '$modelDir/$fileName'; - } - - // Find a model by type - ModelInfo? getModelByType(String type) { - return _models.firstWhere( - (model) => model.type == type, - orElse: () => throw Exception('No model found with type: $type'), - ); - } - - // Get model type string by type - Future getModelTypeString(String type) async { - try { - final model = getModelByType(type); - return model?.modelType; - } catch (e) { - debugPrint('Error getting model type: $e'); - return null; - } - } - - // Check if all required models exist - Future modelsExist() async { - try { - final modelDir = await _modelDirPath; - - // Check for a marker file indicating successful model installation - final markerFile = File('$modelDir/models_installed.txt'); - if (await markerFile.exists()) { - return true; - } - - // Make sure we've loaded the models config - if (_models.isEmpty) { - await _loadModelsFromConfig(); - } - - // Check if all destination files exist for all models - for (var model in _models) { - for (var outputFile in model.outputFiles) { - final file = File('$modelDir/${outputFile.destination}'); - if (!await file.exists()) { - return false; - } - } - } - - // If all files exist but marker doesn't, create the marker - await markerFile.writeAsString('Models installed on ${DateTime.now()}'); - return true; - } catch (e) { - debugPrint('Error checking models: $e'); - return false; - } - } - - // Check if we should only download on Wi-Fi - Future _getWifiOnlySetting() async { - final prefs = await SharedPreferences.getInstance(); - return prefs.getBool('wifi_only_downloads') ?? true; // Default to true - } - - // Save Wi-Fi only setting - Future saveWifiOnlySetting(bool wifiOnly) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool('wifi_only_downloads', wifiOnly); - } - - // Check network connectivity - Future _checkConnectivity() async { - final wifiOnly = await _getWifiOnlySetting(); - - if (!wifiOnly) { - return true; // Download allowed on any connection - } - - // Check for WiFi connection - final connectivityResult = await Connectivity().checkConnectivity(); - return connectivityResult.contains(ConnectivityResult.wifi); - } - - // Show download dialog to user - Future showDownloadDialog(BuildContext context) async { - // Make sure models are loaded - if (_models.isEmpty) { - await _loadModelsFromConfig(); - } - - // Calculate total download size - final totalSize = _models.fold(0.0, (sum, model) => sum + model.size); - - if (!context.mounted) return false; - return await showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext dialogContext) { - return AlertDialog( - title: const Text('Download AI Models'), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'This app requires downloading speech recognition models ' - 'to function properly (${totalSize.toStringAsFixed(1)} MB total).', - ), - const SizedBox(height: 12), - const Text( - 'Models will be downloaded when connected to WiFi. ' - 'You can change this in Settings later.', - ), - const SizedBox(height: 16), - const Text('Models to download:'), - ...List.generate(_models.length, (index) { - final model = _models[index]; - return Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - '• ${model.name} (${model.size.toStringAsFixed(1)} MB)', - style: const TextStyle(fontSize: 14), - ), - ); - }), - ], - ), - ), - actions: [ - TextButton( - child: const Text('Later'), - onPressed: () { - Navigator.of(dialogContext).pop(false); - }, - ), - TextButton( - child: const Text('Download Now'), - onPressed: () { - Navigator.of(dialogContext).pop(true); - }, - ), - ], - ); - }, - ) ?? false; // Default to false if dialog is dismissed - } - - // Show connectivity warning dialog - Future _showConnectivityWarning(BuildContext context) async { - return await showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext dialogContext) { - return AlertDialog( - title: const Text('Wi-Fi Required'), - content: const Text( - 'You have selected to download models only on Wi-Fi. ' - 'Please connect to a Wi-Fi network or change your settings to continue.' - ), - actions: [ - TextButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(dialogContext).pop(false); - }, - ), - TextButton( - child: const Text('Change Setting'), - onPressed: () { - Navigator.of(dialogContext).pop(true); - }, - ), - ], - ); - }, - ) ?? false; // Default to false if dialog is dismissed - } - - // Check if downloads are currently in progress - bool isDownloadInProgress() { - return _toastService.isToastVisible; - } - - // Download all models - Future downloadModels(BuildContext context) async { - // Make sure models are loaded - if (_models.isEmpty) { - await _loadModelsFromConfig(); - - // If still empty after loading, show error - if (_models.isEmpty) { - _toastService.showError('Failed to load model configuration.'); - return false; - } - } - - // Check connectivity first - still need context for initial dialogs - final canDownload = await _checkConnectivity(); - if (!canDownload && context.mounted) { - final changeSettings = await _showConnectivityWarning(context); - if (changeSettings) { - // User wants to change settings - await saveWifiOnlySetting(false); - } else { - // User canceled download - return false; - } - } - - // Start async download process - _startDownloadProcess(); - - // Return true to indicate the download has started - return true; - } - - // Handle the download process independently of any specific context - Future _startDownloadProcess() async { - try { - // Show initial toast notification - _toastService.showToast('Starting downloads...', progress: 0.0); - - final modelDir = await _modelDirPath; - int completedModels = 0; - - // Process each model - for (var model in _models) { - // Update progress - _toastService.showToast( - 'Downloading ${model.name}...', - progress: completedModels / _models.length, - ); - - // Download file - final response = await http.get(Uri.parse(model.url)); - if (response.statusCode != 200) { - throw Exception('Failed to download ${model.name}'); - } - - // Process based on file type - if (model.isCompressed) { - // Update progress - _toastService.showToast( - 'Extracting ${model.name}...', - progress: completedModels / _models.length, - ); - - // Handle compressed .bz2 file - now using isolate - await _processCompressedFileInIsolate( - response.bodyBytes, - modelDir, - model.outputFiles, - ); - } else { - // Handle direct files with mapping to new name - for (var outputFile in model.outputFiles) { - final file = File('$modelDir/${outputFile.destination}'); - await file.writeAsBytes(response.bodyBytes); - } - } - - completedModels++; - _toastService.updateProgress(completedModels / _models.length); - } - - // Create marker file to indicate successful installation - final markerFile = File('$modelDir/models_installed.txt'); - await markerFile.writeAsString('Models installed on ${DateTime.now()}'); - - // Hide toast and show success toast - _toastService.hideToast(); - _toastService.showSuccess('All models have been downloaded successfully.'); - - } catch (e) { - debugPrint('Error downloading models: $e'); - - // Hide progress toast - _toastService.hideToast(); - - // Show error toast - _toastService.showError('Failed to download models: $e'); - } - } - - // Process compressed file in a separate isolate - Future _processCompressedFileInIsolate( - List fileBytes, - String modelDir, - List outputMappings, - ) async { - // Create a ReceivePort for communication - final receivePort = ReceivePort(); - - // Prepare data to send to isolate - final data = ExtractionData( - fileBytes: fileBytes, - modelDir: modelDir, - outputMappings: outputMappings, - sendPort: receivePort.sendPort, - ); - - // Spawn the isolate - await Isolate.spawn(_extractInIsolate, data); - - // Wait for a result from the isolate - await for (final message in receivePort) { - if (message == 'done') { - // Extraction completed - break; - } else if (message is String && message.startsWith('error:')) { - // Error occurred in isolate - throw Exception(message.substring(6)); - } - } - - // Close the port when done - receivePort.close(); - } - - // Isolate entry point for extraction - static Future _extractInIsolate(ExtractionData data) async { - try { - // Decompress bz2 - final archive = BZip2Decoder().decodeBytes(data.fileBytes); - - // Extract tar archive - final tarArchive = TarDecoder().decodeBytes(archive); - - // Create a mapping for efficient lookup - final Map pathMappings = {}; - for (final mapping in data.outputMappings) { - pathMappings[mapping.source] = mapping.destination; - } - - // Track which files we've processed - final Set processedDestinations = {}; - - // Process each file in the archive - for (final file in tarArchive) { - if (!file.isFile) continue; - - // Check for exact matches - String? destinationFile; - - for (final sourcePath in pathMappings.keys) { - if (file.name == sourcePath) { - destinationFile = pathMappings[sourcePath]; - break; - } - } - - // Skip files we don't need - if (destinationFile == null) continue; - - // Mark as processed - processedDestinations.add(destinationFile); - - // Create the destination file - final filePath = '${data.modelDir}/$destinationFile'; - - // Create parent directories if needed - final parentDir = Directory(path.dirname(filePath)); - if (!await parentDir.exists()) { - await parentDir.create(recursive: true); - } - - // Write file - await File(filePath).writeAsBytes(file.content as List); - } - - // Check if all needed files were found - if (processedDestinations.length != pathMappings.length) { - throw Exception('Not all required files were found in the archive'); - } - - // Signal completion - data.sendPort.send('done'); - } catch (e) { - // Send error back to main isolate - data.sendPort.send('error: $e'); - } finally { - // Terminate the isolate - Isolate.exit(); - } - } - - // Delete all downloaded models - Future deleteModels() async { - try { - final modelDir = await _modelDirPath; - final directory = Directory(modelDir); - - if (await directory.exists()) { - // Get all immediate children first - List children = await directory.list().toList(); - - // Delete each child recursively - for (var entity in children) { - try { - await entity.delete(recursive: true); - } catch (e) { - // Log but continue with other deletions - debugPrint('Error deleting ${entity.path}: $e'); - } - } - } - - // Double-check the marker file - final markerFile = File('$modelDir/models_installed.txt'); - if (await markerFile.exists()) { - await markerFile.delete(); - } - - return true; - } catch (e) { - debugPrint('Error deleting models: $e'); - return false; - } - } - - // Trigger model download from settings page - Future downloadModelsFromSettings(BuildContext context) async { - final shouldDownload = await showDownloadDialog(context); - if (shouldDownload && context.mounted) { - return await downloadModels(context); - } - return false; - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/services/models.dart b/team_b/yappy/lib/services/models.dart deleted file mode 100644 index 501675d4..00000000 --- a/team_b/yappy/lib/services/models.dart +++ /dev/null @@ -1,1264 +0,0 @@ -import 'dart:convert'; - -/// A base class for a request to the Amazon Transcribe Streaming API. -abstract class TranscribeStreamingRequest { - /// Creates a [TranscribeStreamingRequest]. - const TranscribeStreamingRequest(); - - /// The path for the request, for example `/stream-transcription` - String get path; - - /// The target for the request, - /// for example `com.amazonaws.transcribe.Transcribe.StartStreamTranscription` - String get target; - - /// The duration of each audio chunk in milliseconds. - int get chunkDurationMs; - - /// The chunk size for the audio stream. Zero value disables chunking. - int get chunkSize; - - /// Returns the headers for the request. - Map toHeaders(); -} - -/// Starts a HTTP/2 stream where audio is streamed to Amazon Transcribe -/// and the transcription results are streamed to your application. -class StartStreamTranscriptionRequest extends TranscribeStreamingRequest { - /// Specifies the language code that represents the language spoken - /// in your audio. - /// - /// If you're unsure of the language spoken in your audio, consider using - /// [identifyLanguage] to enable automatic language identification. - /// - /// For a list of languages supported with Amazon Transcribe streaming, refer - /// to the [Supported languages](https://docs.aws.amazon.com/transcribe/latest/dg/supported-languages.html) table. - final LanguageCode? languageCode; - - /// The sample rate of the input audio (in hertz). - /// - /// Low-quality audio, such as telephone audio, is typically around 8,000 Hz. - /// - /// High-quality audio typically ranges from 16,000 Hz to 48,000 Hz. - /// - /// Note that the sample rate you specify must match that of your audio. - final int mediaSampleRateHertz; - - /// Specifies the encoding of your input audio. - /// - /// Supported formats are: - /// * FLAC - /// * OPUS-encoded audio in an Ogg container - /// * PCM (only signed 16-bit little-endian audio formats, which does not include WAV) - /// - /// For more information, see - /// [Media formats](https://docs.aws.amazon.com/transcribe/latest/dg/how-input.html#how-input-audio). - final MediaEncoding mediaEncoding; - - /// Specifies the name of the custom vocabulary that you want to use - /// when processing your transcription. - /// - /// Note that vocabulary names are case sensitive. - /// - /// If the language of the specified custom vocabulary doesn't match - /// the language identified in your media, the custom vocabulary - /// is not applied to your transcription. - /// - /// This parameter is **not** intended for use with the [identifyLanguage] - /// parameter If you're including [identifyLanguage] in your request and - /// want to use one or more custom vocabularies with your transcription, - /// use the [vocabularyNames] parameter instead. - /// - /// For more information, see [Custom vocabularies](https://docs.aws.amazon.com/transcribe/latest/dg/custom-vocabulary.html). - final String? vocabularyName; - - /// Specifies a name for your transcription session. - /// - /// If you don't include this parameter in your request, Amazon Transcribe - /// generates an ID and returns it in the response. - /// - /// You can use a session ID to retry a streaming session. - final String? sessionId; - - /// Specifies the name of the custom vocabulary filter that you want to use - /// when processing your transcription. - /// - /// Note that vocabulary filter names are case sensitive. - /// - /// If the language of the specified custom vocabulary filter doesn't match - /// the language identified in your media, the vocabulary filter - /// is not applied to your transcription. - /// - /// This parameter is **not** intended for use with the [identifyLanguage] - /// parameter If you're including [identifyLanguage] in your request and - /// want to use one or more vocabulary filters with your transcription, - /// use the [vocabularyFilterNames] parameter instead. - /// - /// For more information, see [Using vocabulary filtering with unwanted words](https://docs.aws.amazon.com/transcribe/latest/dg/vocabulary-filtering.html). - final String? vocabularyFilterName; - - /// Specifies how you want your vocabulary filter applied to your transcript. - /// - /// To replace words with `***`, choose `mask`. - /// - /// To delete words, choose `remove`. - /// - /// To flag words without changing them, choose `tag`. - final VocabularyFilterMethod? vocabularyFilterMethod; - - /// Enables speaker partitioning (diarization) in your transcription output. - /// - /// Speaker partitioning labels the speech from individual speakers in your - /// media file. - /// - /// For more information, see [Partitioning speakers (diarization)](https://docs.aws.amazon.com/transcribe/latest/dg/diarization.html). - final bool? showSpeakerLabel; - - /// Enables channel identification in multi-channel audio. - /// - /// Channel identification transcribes the audio on each channel - /// independently, then appends the output for each channel into - /// one transcript. - /// - /// If you have multi-channel audio and do not enable channel identification, - /// your audio is transcribed in a continuous manner and your transcript - /// is not separated by channel. - /// - /// For more information, see [Transcribing multi-channel audio](https://docs.aws.amazon.com/transcribe/latest/dg/channel-id.html). - final bool? enableChannelIdentification; - - /// Specifies the number of channels in your audio stream. - /// - /// Up to two channels are supported. - final int? numberOfChannels; - - /// Enables partial result stabilization for your transcription. - /// - /// Partial result stabilization can reduce latency in your output, - /// but may impact accuracy. - /// - /// For more information, see [Partial-result stabilization](https://docs.aws.amazon.com/transcribe/latest/dg/streaming.html#streaming-partial-result-stabilization). - final bool? enablePartialResultsStabilization; - - /// Specifies the level of stability to use when you enable partial results - /// stabilization. - /// - /// Low stability provides the highest accuracy. - /// High stability transcribes faster, but with slightly lower accuracy. - /// - /// For more information, see [Partial-result stabilization](https://docs.aws.amazon.com/transcribe/latest/dg/streaming.html#streaming-partial-result-stabilization). - final PartialResultsStability? partialResultsStability; - - /// Labels all personally identifiable information (PII) identified - /// in your transcript. - /// - /// Content identification is performed at the segment level. - /// PII specified in [piiEntityTypes] is flagged upon complete transcription - /// of an audio segment. - /// - /// You can’t set [contentIdentificationType] and [contentRedactionType] - /// in the same request. If you set both, your request returns a - /// `BadRequestException`. - /// - /// For more information, see [Redacting or identifying personally identifiable information](https://docs.aws.amazon.com/transcribe/latest/dg/pii-redaction.html). - final ContentIdentificationType? contentIdentificationType; - - /// Redacts all personally identifiable information (PII) identified - /// in your transcript. - /// - /// Content redaction is performed at the segment level. - /// PII specified in [piiEntityTypes] is redacted upon complete transcription - /// of an audio segment. - /// - /// You can’t set [contentRedactionType] and [contentIdentificationType] - /// in the same request. If you set both, your request returns a - /// `BadRequestException`. - /// - /// For more information, see [Redacting or identifying personally identifiable information](https://docs.aws.amazon.com/transcribe/latest/dg/pii-redaction.html). - final ContentRedactionType? contentRedactionType; - - /// Specifies which types of personally identifiable information (PII) - /// you want to redact in your transcript. - /// - /// You can include as many types as you'd like, or you can select `ALL`. - /// - /// To include [piiEntityTypes] in your request, you must also include either - /// [contentIdentificationType] or [contentRedactionType]. - /// - /// Values must be comma-separated and can include: - /// * `BANK_ACCOUNT_NUMBER` - /// * `BANK_ROUTING` - /// * `CREDIT_DEBIT_NUMBER` - /// * `CREDIT_DEBIT_CVV` - /// * `CREDIT_DEBIT_EXPIRY` - /// * `PIN` - /// * `EMAIL` - /// * `ADDRESS` - /// * `NAME` - /// * `PHONE` - /// * `SSN` - /// * `ALL` - final String? piiEntityTypes; - - /// Specifies the name of the custom language model that you want to use - /// when processing your transcription. - /// - /// Note that language model names are case sensitive. - /// - /// The language of the specified language model must match the language code - /// you specify in your transcription request. If the languages don't match, - /// the custom language model isn't applied. There are no errors or warnings - /// associated with a language mismatch. - /// - /// For more information, see [Custom language models](https://docs.aws.amazon.com/transcribe/latest/dg/custom-language-models.html). - final String? languageModelName; - - /// Enables automatic language identification for your transcription. - /// - /// If you include [identifyLanguage], you can optionally include a list - /// of language codes, using [languageOptions], that you think may be present - /// in your audio stream. Including language options can improve - /// transcription accuracy. - /// - /// You can also include a preferred language using [preferredLanguage]. - /// Adding a preferred language can help Amazon Transcribe identify - /// the language faster than if you omit this parameter. - /// - /// If you have multi-channel audio that contains different languages - /// on each channel, and you've enabled channel identification, - /// automatic language identification identifies the dominant language - /// on each audio channel. - /// - /// Note that you must include either [languageCode] or [identifyLanguage] - /// or [identifyMultipleLanguages] in your request. If you include more than - /// one of these parameters, your transcription job fails. - /// - /// Streaming language identification can't be combined with custom language - /// models or redaction. - final bool? identifyLanguage; - - /// Specifies two or more language codes that represent the languages - /// you think may be present in your media. - /// - /// Including more than five is not recommended. If you're unsure - /// what languages are present, do not include this parameter. - /// - /// Including language options can improve the accuracy of language - /// identification. - /// - /// If you include [languageOptions] in your request, you must also - /// include [identifyLanguage]. - /// - /// For a list of languages supported with Amazon Transcribe streaming, - /// refer to the [Supported languages](https://docs.aws.amazon.com/transcribe/latest/dg/supported-languages.html) - /// - /// You can only include one language dialect per language per stream. - /// For example, you cannot include `en-US` and `en-AU` in the same request. - final String? languageOptions; - - /// Specifies a preferred language from the subset of languages codes - /// you specified in [languageOptions]. - /// - /// You can only use this parameter if you've included [identifyLanguage] - /// and [languageOptions] in your request. - final String? preferredLanguage; - - /// Enables automatic multi-language identification in your transcription - /// job request. - /// - /// Use this parameter if your stream contains more than one language. - /// If your stream contains only one language, use IdentifyLanguage instead. - /// - /// If you include [identifyMultipleLanguages], you can optionally include - /// a list of language codes, using [languageOptions], that you think may be - /// present in your stream. - /// Including [languageOptions] restricts [identifyMultipleLanguages] to - /// only the language options that you specify, which can improve - /// transcription accuracy. - /// - /// If you want to apply a custom vocabulary or a custom vocabulary filter - /// to your automatic multiple language identification request, include - /// [vocabularyNames] or [vocabularyFilterNames]. - /// - /// Note that you must include one of [languageCode], [identifyLanguage], - /// or [identifyMultipleLanguages] in your request. If you include more than - /// one of these parameters, your transcription job fails. - final bool? identifyMultipleLanguages; - - /// Specifies the names of the custom vocabularies that you want to use - /// when processing your transcription. - /// - /// Note that vocabulary names are case sensitive. - /// - /// If none of the languages of the specified custom vocabularies match - /// the language identified in your media, your job fails. - /// - /// This parameter is only intended for use **with** the [identifyLanguage] - /// parameter. If you're **not** including [identifyLanguage] in your request - /// and want to use a custom vocabulary with your transcription, - /// use the [vocabularyName] parameter instead. - /// - /// For more information, see [Custom vocabularies](https://docs.aws.amazon.com/transcribe/latest/dg/custom-vocabulary.html). - final String? vocabularyNames; - - /// Specifies the names of the custom vocabulary filters that you want to use - /// when processing your transcription. - /// - /// Note that vocabulary filter names are case sensitive. - /// - /// If none of the languages of the specified custom vocabulary filters match - /// the language identified in your media, your job fails. - /// - /// This parameter is only intended for use **with** the [identifyLanguage] - /// parameter. If you're **not** including [identifyLanguage] in your request - /// and want to use a custom vocabulary filter with your transcription, - /// use the [vocabularyFilterName] parameter instead. - /// - /// For more information, see [Using vocabulary filtering with unwanted words](https://docs.aws.amazon.com/transcribe/latest/dg/vocabulary-filtering.html). - final String? vocabularyFilterNames; - - /// Creates a [StartStreamTranscriptionRequest] to start a streaming - /// transcription. - const StartStreamTranscriptionRequest({ - this.languageCode, - required this.mediaSampleRateHertz, - required this.mediaEncoding, - this.vocabularyName, - this.sessionId, - this.vocabularyFilterName, - this.vocabularyFilterMethod, - this.showSpeakerLabel, - this.enableChannelIdentification, - this.numberOfChannels, - this.enablePartialResultsStabilization, - this.partialResultsStability, - this.contentIdentificationType, - this.contentRedactionType, - this.piiEntityTypes, - this.languageModelName, - this.identifyLanguage, - this.languageOptions, - this.preferredLanguage, - this.identifyMultipleLanguages, - this.vocabularyNames, - this.vocabularyFilterNames, - }) : assert(languageCode != null || - identifyLanguage != null || - identifyMultipleLanguages != null), - assert(mediaSampleRateHertz >= 8000 && mediaSampleRateHertz <= 48000); - - @override - String get path => '/stream-transcription'; - - @override - String get target => - 'com.amazonaws.transcribe.Transcribe.StartStreamTranscription'; - - @override - int get chunkDurationMs => 200; - - @override - int get chunkSize => switch (mediaEncoding) { - MediaEncoding.pcm => mediaSampleRateHertz * 2 * chunkDurationMs ~/ 1000, - _ => 0, - }; - - @override - Map toHeaders() => { - if (languageCode != null) - 'x-amzn-transcribe-language-code': languageCode!.value, - 'x-amzn-transcribe-sample-rate': mediaSampleRateHertz.toString(), - 'x-amzn-transcribe-media-encoding': mediaEncoding.value, - if (vocabularyName != null) - 'x-amzn-transcribe-vocabulary-name': vocabularyName!, - if (sessionId != null) 'x-amzn-transcribe-session-id': sessionId!, - if (vocabularyFilterName != null) - 'x-amzn-transcribe-vocabulary-filter-name': vocabularyFilterName!, - if (vocabularyFilterMethod != null) - 'x-amzn-transcribe-vocabulary-filter-method': - vocabularyFilterMethod!.value, - if (showSpeakerLabel != null) - 'x-amzn-transcribe-show-speaker-label': showSpeakerLabel!.toString(), - if (enableChannelIdentification != null) - 'x-amzn-transcribe-enable-channel-identification': - enableChannelIdentification!.toString(), - if (numberOfChannels != null) - 'x-amzn-transcribe-number-of-channels': numberOfChannels!.toString(), - if (enablePartialResultsStabilization != null) - 'x-amzn-transcribe-enable-partial-results-stabilization': - enablePartialResultsStabilization!.toString(), - if (partialResultsStability != null) - 'x-amzn-transcribe-partial-results-stability': - partialResultsStability!.value, - if (contentIdentificationType != null) - 'x-amzn-transcribe-content-identification-type': - contentIdentificationType!.value, - if (contentRedactionType != null) - 'x-amzn-transcribe-content-redaction-type': - contentRedactionType!.value, - if (piiEntityTypes != null) - 'x-amzn-transcribe-pii-entity-types': piiEntityTypes!, - if (languageModelName != null) - 'x-amzn-transcribe-language-model-name': languageModelName!, - if (identifyLanguage != null) - 'x-amzn-transcribe-identify-language': identifyLanguage!.toString(), - if (languageOptions != null) - 'x-amzn-transcribe-language-options': languageOptions!, - if (preferredLanguage != null) - 'x-amzn-transcribe-preferred-language': preferredLanguage!, - if (identifyMultipleLanguages != null) - 'x-amzn-transcribe-identify-multiple-languages': - identifyMultipleLanguages!.toString(), - if (vocabularyNames != null) - 'x-amzn-transcribe-vocabulary-names': vocabularyNames!, - if (vocabularyFilterNames != null) - 'x-amzn-transcribe-vocabulary-filter-names': vocabularyFilterNames!, - }; -} - -/// Response for the [StartStreamTranscriptionRequest]. -final class StartStreamTranscriptionResponse { - /// Provides the identifier for your streaming request. - final String? requestId; - - /// Provides the language code that you specified in your request. - final LanguageCode? languageCode; - - /// Provides the sample rate that you specified in your request. - final int? mediaSampleRateHertz; - - /// Provides the media encoding you specified in your request. - final MediaEncoding? mediaEncoding; - - /// Provides the name of the custom vocabulary that you specified - /// in your request. - final String? vocabularyName; - - /// Provides the identifier for your transcription session. - final String? sessionId; - - /// Provides the name of the custom vocabulary filter that you specified - /// in your request. - final String? vocabularyFilterName; - - /// Provides the vocabulary filtering method used in your transcription. - final VocabularyFilterMethod? vocabularyFilterMethod; - - /// Shows whether speaker partitioning was enabled for your transcription. - final bool? showSpeakerLabel; - - /// Shows whether channel identification was enabled for your transcription. - final bool? enableChannelIdentification; - - /// Provides the number of channels that you specified in your request. - final int? numberOfChannels; - - /// Shows whether partial results stabilization was enabled - /// for your transcription. - final bool? enablePartialResultsStabilization; - - /// Provides the stabilization level used for your transcription. - final PartialResultsStability? partialResultsStability; - - /// Shows whether content identification was enabled for your transcription. - final ContentIdentificationType? contentIdentificationType; - - /// Shows whether content redaction was enabled for your transcription. - final ContentRedactionType? contentRedactionType; - - /// Lists the PII entity types you specified in your request. - final String? piiEntityTypes; - - /// Provides the name of the custom language model that you specified - /// in your request. - final String? languageModelName; - - /// Shows whether automatic language identification was enabled - /// for your transcription. - final bool? identifyLanguage; - - /// Provides the language codes that you specified in your request. - final String? languageOptions; - - /// Provides the preferred language that you specified in your request. - final LanguageCode? preferredLanguage; - - /// Shows whether automatic multi-language identification was enabled - /// for your transcription. - final bool? identifyMultipleLanguages; - - /// Provides the names of the custom vocabularies that you specified - /// in your request. - final String? vocabularyNames; - - /// Provides the names of the custom vocabulary filters that you specified - /// in your request. - final String? vocabularyFilterNames; - - /// Creates a [StartStreamTranscriptionResponse] from the values of - /// the headers of a response from the Amazon Transcribe Streaming API. - const StartStreamTranscriptionResponse({ - this.requestId, - this.languageCode, - this.mediaSampleRateHertz, - this.mediaEncoding, - this.vocabularyName, - this.sessionId, - this.vocabularyFilterName, - this.vocabularyFilterMethod, - this.showSpeakerLabel, - this.enableChannelIdentification, - this.numberOfChannels, - this.enablePartialResultsStabilization, - this.partialResultsStability, - this.contentIdentificationType, - this.contentRedactionType, - this.piiEntityTypes, - this.languageModelName, - this.identifyLanguage, - this.languageOptions, - this.preferredLanguage, - this.identifyMultipleLanguages, - this.vocabularyNames, - this.vocabularyFilterNames, - }); - - /// Creates a [StartStreamTranscriptionResponse] from the headers of a - /// response from the Amazon Transcribe Streaming API. - factory StartStreamTranscriptionResponse.fromHeaders( - Map headers) { - return StartStreamTranscriptionResponse( - requestId: headers['x-amzn-request-id'], - languageCode: headers['x-amzn-transcribe-language-code'] != null - ? LanguageCode.fromValue(headers['x-amzn-transcribe-language-code']!) - : null, - mediaSampleRateHertz: headers['x-amzn-transcribe-sample-rate'] != null - ? int.parse(headers['x-amzn-transcribe-sample-rate']!) - : null, - mediaEncoding: headers['x-amzn-transcribe-media-encoding'] != null - ? MediaEncoding.fromValue( - headers['x-amzn-transcribe-media-encoding']!) - : null, - vocabularyName: headers['x-amzn-transcribe-vocabulary-name'], - sessionId: headers['x-amzn-transcribe-session-id'], - vocabularyFilterName: headers['x-amzn-transcribe-vocabulary-filter-name'], - vocabularyFilterMethod: - headers['x-amzn-transcribe-vocabulary-filter-method'] != null - ? VocabularyFilterMethod.fromValue( - headers['x-amzn-transcribe-vocabulary-filter-method']!) - : null, - showSpeakerLabel: headers['x-amzn-transcribe-show-speaker-label'] != null - ? headers['x-amzn-transcribe-show-speaker-label'] == 'true' - : null, - enableChannelIdentification: - headers['x-amzn-transcribe-enable-channel-identification'] != null - ? headers['x-amzn-transcribe-enable-channel-identification'] == - 'true' - : null, - numberOfChannels: headers['x-amzn-transcribe-number-of-channels'] != null - ? int.parse(headers['x-amzn-transcribe-number-of-channels']!) - : null, - enablePartialResultsStabilization: headers[ - 'x-amzn-transcribe-enable-partial-results-stabilization'] != - null - ? headers['x-amzn-transcribe-enable-partial-results-stabilization'] == - 'true' - : null, - partialResultsStability: - headers['x-amzn-transcribe-partial-results-stability'] != null - ? PartialResultsStability.fromValue( - headers['x-amzn-transcribe-partial-results-stability']!) - : null, - contentIdentificationType: - headers['x-amzn-transcribe-content-identification-type'] != null - ? ContentIdentificationType.fromValue( - headers['x-amzn-transcribe-content-identification-type']!) - : null, - contentRedactionType: - headers['x-amzn-transcribe-content-redaction-type'] != null - ? ContentRedactionType.fromValue( - headers['x-amzn-transcribe-content-redaction-type']!) - : null, - piiEntityTypes: headers['x-amzn-transcribe-pii-entity-types'], - languageModelName: headers['x-amzn-transcribe-language-model-name'], - identifyLanguage: headers['x-amzn-transcribe-identify-language'] != null - ? headers['x-amzn-transcribe-identify-language'] == 'true' - : null, - languageOptions: headers['x-amzn-transcribe-language-options'], - preferredLanguage: headers['x-amzn-transcribe-preferred-language'] != null - ? LanguageCode.fromValue( - headers['x-amzn-transcribe-preferred-language']!) - : null, - identifyMultipleLanguages: - headers['x-amzn-transcribe-identify-multiple-languages'] != null - ? headers['x-amzn-transcribe-identify-multiple-languages'] == - 'true' - : null, - vocabularyNames: headers['x-amzn-transcribe-vocabulary-names'], - vocabularyFilterNames: - headers['x-amzn-transcribe-vocabulary-filter-names'], - ); - } - - /// Returns the headers for the response. - Map toHeaders() { - return { - if (requestId != null) 'x-amzn-request-id': requestId, - if (languageCode != null) - 'x-amzn-transcribe-language-code': languageCode?.value, - if (mediaSampleRateHertz != null) - 'x-amzn-transcribe-sample-rate': mediaSampleRateHertz, - if (mediaEncoding != null) - 'x-amzn-transcribe-media-encoding': mediaEncoding?.value, - if (vocabularyName != null) - 'x-amzn-transcribe-vocabulary-name': vocabularyName, - if (sessionId != null) 'x-amzn-transcribe-session-id': sessionId, - if (vocabularyFilterName != null) - 'x-amzn-transcribe-vocabulary-filter-name': vocabularyFilterName, - if (vocabularyFilterMethod != null) - 'x-amzn-transcribe-vocabulary-filter-method': - vocabularyFilterMethod?.value, - if (showSpeakerLabel != null) - 'x-amzn-transcribe-show-speaker-label': showSpeakerLabel, - if (enableChannelIdentification != null) - 'x-amzn-transcribe-enable-channel-identification': - enableChannelIdentification, - if (numberOfChannels != null) - 'x-amzn-transcribe-number-of-channels': numberOfChannels, - if (enablePartialResultsStabilization != null) - 'x-amzn-transcribe-enable-partial-results-stabilization': - enablePartialResultsStabilization, - if (partialResultsStability != null) - 'x-amzn-transcribe-partial-results-stability': - partialResultsStability?.value, - if (contentIdentificationType != null) - 'x-amzn-transcribe-content-identification-type': - contentIdentificationType?.value, - if (contentRedactionType != null) - 'x-amzn-transcribe-content-redaction-type': contentRedactionType?.value, - if (piiEntityTypes != null) - 'x-amzn-transcribe-pii-entity-types': piiEntityTypes, - if (languageModelName != null) - 'x-amzn-transcribe-language-model-name': languageModelName, - if (identifyLanguage != null) - 'x-amzn-transcribe-identify-language': identifyLanguage, - if (languageOptions != null) - 'x-amzn-transcribe-language-options': languageOptions, - if (preferredLanguage != null) - 'x-amzn-transcribe-preferred-language': preferredLanguage?.value, - if (identifyMultipleLanguages != null) - 'x-amzn-transcribe-identify-multiple-languages': - identifyMultipleLanguages, - if (vocabularyNames != null) - 'x-amzn-transcribe-vocabulary-names': vocabularyNames, - if (vocabularyFilterNames != null) - 'x-amzn-transcribe-vocabulary-filter-names': vocabularyFilterNames, - }; - } -} - -/// Possible values for the `languageCode` parameter of a -/// [StartStreamTranscriptionRequest]. -enum LanguageCode { - deDe('de-DE'), - enAu('en-AU'), - enGb('en-GB'), - enUs('en-US'), - esUs('es-US'), - frCa('fr-CA'), - frFr('fr-FR'), - hiIn('hi-IN'), - itIt('it-IT'), - jaJp('ja-JP'), - koKr('ko-KR'), - ptBr('pt-BR'), - thTh('th-TH'), - zhCn('zh-CN'); - - /// Creates a [LanguageCode] with the given value. - const LanguageCode(this.value); - - /// The language code value. - final String value; - - /// Returns the [LanguageCode] for the given value. - factory LanguageCode.fromValue(String value) { - return LanguageCode.values.firstWhere((e) => e.value == value); - } -} - -/// Possible values for the `mediaEncoding` parameter of a -/// [StartStreamTranscriptionRequest]. -enum MediaEncoding { - flac('flac'), - oggOpus('ogg-opus'), - pcm('pcm'); - - /// Creates a [MediaEncoding] with the given value. - const MediaEncoding(this.value); - - /// The media encoding value. - final String value; - - /// Returns the [MediaEncoding] for the given value. - factory MediaEncoding.fromValue(String value) { - return MediaEncoding.values.firstWhere((e) => e.value == value); - } -} - -/// Possible values for the `vocabularyFilterMethod` parameter of a -/// [StartStreamTranscriptionRequest]. -enum VocabularyFilterMethod { - mask('mask'), - remove('remove'), - tag('tag'); - - /// Creates a [VocabularyFilterMethod] with the given value. - const VocabularyFilterMethod(this.value); - - /// The vocabulary filter method value. - final String value; - - /// Returns the [VocabularyFilterMethod] for the given value. - factory VocabularyFilterMethod.fromValue(String value) { - return VocabularyFilterMethod.values.firstWhere((e) => e.value == value); - } -} - -/// Possible values for the `partialResultsStability` parameter of a -/// [StartStreamTranscriptionRequest]. -enum PartialResultsStability { - high('high'), - low('low'), - medium('medium'); - - /// Creates a [PartialResultsStability] with the given value. - const PartialResultsStability(this.value); - - /// The partial results stability value. - final String value; - - /// Returns the [PartialResultsStability] for the given value. - factory PartialResultsStability.fromValue(String value) { - return PartialResultsStability.values.firstWhere((e) => e.value == value); - } -} - -/// Possible values for the `contentIdentificationType` parameter of a -/// [StartStreamTranscriptionRequest]. -enum ContentIdentificationType { - pII('PII'); - - /// Creates a [ContentIdentificationType] with the given value. - const ContentIdentificationType(this.value); - - /// The content identification type value. - final String value; - - /// Returns the [ContentIdentificationType] for the given value. - factory ContentIdentificationType.fromValue(String value) { - return ContentIdentificationType.values.firstWhere((e) => e.value == value); - } -} - -/// Possible values for the `contentRedactionType` parameter of a -/// [StartStreamTranscriptionRequest]. -enum ContentRedactionType { - pII('PII'); - - /// Creates a [ContentRedactionType] with the given value. - const ContentRedactionType(this.value); - - /// The content redaction type value. - final String value; - - /// Returns the [ContentRedactionType] for the given value. - factory ContentRedactionType.fromValue(String value) { - return ContentRedactionType.values.firstWhere((e) => e.value == value); - } -} - -/// The `TranscriptEvent` associated with a `transcriptEventStream` -/// returned from [TranscribeStreamingClient.startStreamTranscription]. -/// -/// Contains a set of transcription results from one or more audio segments, -/// along with additional information per your request parameters. -final class TranscriptEvent { - /// Contains `Results`, which contains a set of transcription results from - /// one or more audio segments, along with additional information per your - /// request parameters. This can include information relating to alternative - /// transcriptions, channel identification, partial result stabilization, - /// language identification, and other transcription-related data. - final Transcript? transcript; - - /// Creates a [TranscriptEvent] from the given values. - const TranscriptEvent({ - this.transcript, - }); - - /// Creates a [TranscriptEvent] from the given [Map]. - factory TranscriptEvent.fromMap(Map map) { - return TranscriptEvent( - transcript: map['Transcript'] != null - ? Transcript.fromMap(map['Transcript'] as Map) - : null, - ); - } - - /// Creates a [TranscriptEvent] from the given JSON string. - factory TranscriptEvent.fromJson(String source) => - TranscriptEvent.fromMap(json.decode(source) as Map); - - /// Returns the [Map] representation of this [TranscriptEvent]. - Map toMap() { - return { - 'Transcript': transcript?.toMap(), - }; - } - - /// Returns the JSON string representation of this [TranscriptEvent]. - String toJson() => json.encode(toMap()); -} - -/// The `Transcript` associated with a [TranscriptEvent]. -/// -/// [Transcript] contains [Result]s, which contains a set of transcription -/// results from one or more audio segments, along with additional information -/// per your request parameters. -final class Transcript { - /// Contains a set of transcription results from one or more audio segments, - /// along with additional information per your request parameters. This can - /// include information relating to alternative transcriptions, channel - /// identification, partial result stabilization, language identification, - /// and other transcription-related data. - final List? results; - - /// Creates a [Transcript] from the given values. - const Transcript({ - this.results, - }); - - /// Creates a [Transcript] from the given [Map]. - factory Transcript.fromMap(Map map) { - return Transcript( - results: map['Results'] != null - ? List.from( - (map['Results'] as List).map( - (x) => Result.fromMap(x as Map), - ), - ) - : null, - ); - } - - /// Creates a [Transcript] from the given JSON string. - factory Transcript.fromJson(String source) => - Transcript.fromMap(json.decode(source) as Map); - - /// Returns the [Map] representation of this [Transcript]. - Map toMap() { - return { - 'Results': results?.map((x) => x.toMap()).toList(), - }; - } - - /// Returns the JSON string representation of this [Transcript]. - String toJson() => json.encode(toMap()); -} - -/// The `Result` associated with a [TranscriptEvent]. -/// -/// Contains a set of transcription results from one or more audio segments, -/// along with additional information per your request parameters. This can -/// include information relating to alternative transcriptions, channel -/// identification, partial result stabilization, language identification, -/// and other transcription-related data. -final class Result { - /// Provides a unique identifier for the [Result]. - final String? resultId; - - /// The start time, in milliseconds, of the [Result]. - final num? startTime; - - /// The end time, in milliseconds, of the [Result]. - final num? endTime; - - /// Indicates if the segment is complete. - /// - /// If [isPartial] is `true`, the segment is not complete. - /// If [isPartial] is `false`, the segment is complete. - final bool? isPartial; - - /// A list of possible alternative transcriptions for the input audio. - /// - /// Each alternative may contain one or more of [Item], [Entity], - /// or [Transcript]. - final List? alternatives; - - /// Indicates which audio channel is associated with the [Result]. - final String? channelId; - - /// The language code that represents the language spoken in your stream. - final LanguageCode? languageCode; - - /// The language code of the dominant language identified in your stream. - /// - /// If you enabled channel identification and each channel of your audio - /// contains a different language, you may have more than one result. - final List? languageIdentification; - - /// Creates a [Result] from the given values. - const Result({ - this.resultId, - this.startTime, - this.endTime, - this.isPartial, - this.alternatives, - this.channelId, - this.languageCode, - this.languageIdentification, - }); - - /// Creates a [Result] from the given [Map]. - factory Result.fromMap(Map map) { - return Result( - resultId: map['ResultId'] != null ? map['ResultId'] as String : null, - startTime: map['StartTime'] != null ? map['StartTime'] as num : null, - endTime: map['EndTime'] != null ? map['EndTime'] as num : null, - isPartial: map['IsPartial'] != null ? map['IsPartial'] as bool : null, - alternatives: map['Alternatives'] != null - ? List.from( - (map['Alternatives'] as List).map( - (x) => Alternative.fromMap(x as Map), - ), - ) - : null, - channelId: map['ChannelId'] != null ? map['ChannelId'] as String : null, - languageCode: map['LanguageCode'] != null - ? LanguageCode.fromValue(map['LanguageCode'] as String) - : null, - languageIdentification: map['LanguageIdentification'] != null - ? List.from( - (map['LanguageIdentification'] as List) - .map( - (x) => LanguageWithScore.fromMap(x as Map), - ), - ) - : null, - ); - } - - /// Creates a [Result] from the given JSON string. - factory Result.fromJson(String source) => - Result.fromMap(json.decode(source) as Map); - - /// Returns the [Map] representation of this [Result]. - Map toMap() { - return { - 'ResultId': resultId, - 'StartTime': startTime, - 'EndTime': endTime, - 'IsPartial': isPartial, - 'Alternatives': alternatives?.map((x) => x.toMap()).toList(), - 'ChannelId': channelId, - 'LanguageCode': languageCode?.value, - 'LanguageIdentification': - languageIdentification?.map((x) => x.toMap()).toList(), - }; - } - - /// Returns the JSON string representation of this [Result]. - String toJson() => json.encode(toMap()); -} - -/// The language code that represents the language identified in your audio, -/// including the associated confidence score. -/// -/// If you enabled channel identification in your request and each channel -/// contained a different language, you will have more than one -/// [LanguageWithScore] result. -final class LanguageWithScore { - /// The language code of the identified language. - final LanguageCode? languageCode; - - /// The confidence score associated with the identified language code. - /// - /// Confidence scores are values between zero and one; larger values indicate - /// a higher confidence in the identified language. - final double? score; - - /// Creates a [LanguageWithScore] from the given values. - const LanguageWithScore({ - this.languageCode, - this.score, - }); - - /// Creates a [LanguageWithScore] from the given [Map]. - factory LanguageWithScore.fromMap(Map map) { - return LanguageWithScore( - languageCode: map['LanguageCode'] != null - ? LanguageCode.fromValue(map['LanguageCode'] as String) - : null, - score: map['Score'] != null ? map['Score'] as double : null, - ); - } - - /// Creates a [LanguageWithScore] from the given JSON string. - factory LanguageWithScore.fromJson(String source) => - LanguageWithScore.fromMap(json.decode(source) as Map); - - /// Returns the [Map] representation of this [LanguageWithScore]. - Map toMap() { - return { - 'LanguageCode': languageCode?.value, - 'Score': score, - }; - } - - /// Returns the JSON string representation of this [LanguageWithScore]. - String toJson() => json.encode(toMap()); -} - -/// A list of possible alternative transcriptions for the input audio. -/// -/// Each alternative may contain one or more of [Item], [Entity], -/// or [Transcript]. -final class Alternative { - /// Contains transcribed text. - final String? transcript; - - /// Contains words, phrases, or punctuation marks in your transcription - /// output. - final List? items; - - /// Contains entities identified as personally identifiable information (PII) - /// in your transcription output. - final List? entities; - - /// Creates an [Alternative] from the given values. - const Alternative({ - this.transcript, - this.items, - this.entities, - }); - - /// Creates an [Alternative] from the given [Map]. - factory Alternative.fromMap(Map map) { - return Alternative( - transcript: - map['Transcript'] != null ? map['Transcript'] as String : null, - items: map['Items'] != null - ? List.from( - (map['Items'] as List).map( - (x) => Item.fromMap(x as Map), - ), - ) - : null, - entities: map['Entities'] != null - ? List.from( - (map['Entities'] as List).map( - (x) => Entity.fromMap(x as Map), - ), - ) - : null, - ); - } - - /// Creates an [Alternative] from the given JSON string. - factory Alternative.fromJson(String source) => - Alternative.fromMap(json.decode(source) as Map); - - /// Returns the [Map] representation of this [Alternative]. - Map toMap() { - return { - 'Transcript': transcript, - 'Items': items?.map((x) => x.toMap()).toList(), - 'Entities': entities?.map((x) => x.toMap()).toList(), - }; - } - - /// Returns the JSON string representation of this [Alternative]. - String toJson() => json.encode(toMap()); -} - -/// A word, phrase, or punctuation mark in your transcription output, -/// along with various associated attributes, such as confidence score, type, -/// and start and end times. -final class Item { - /// The start time, in milliseconds, of the transcribed item. - final num? startTime; - - /// The end time, in milliseconds, of the transcribed item. - final num? endTime; - - /// The type of item identified. Options are: `PRONUNCIATION` (spoken words) - /// and `PUNCTUATION`. - final ItemType? type; - - /// The word or punctuation that was transcribed. - final String? content; - - /// Indicates whether the specified item matches a word in the vocabulary - /// filter included in your request. - /// - /// If `true`, there is a vocabulary filter match. - final bool? vocabularyFilterMatch; - - /// If speaker partitioning is enabled, [speaker] labels the speaker of - /// the specified item. - final String? speaker; - - /// The confidence score associated with a word or phrase in your transcript. - /// - /// Confidence scores are values between 0 and 1. A larger value indicates - /// a higher probability that the identified item correctly matches - /// the item spoken in your media. - final double? confidence; - - /// If partial result stabilization is enabled, [stable] indicates whether - /// the specified item is stable (`true`) or if it may change when the segment - /// is complete (`false`). - final bool? stable; - - /// Creates an [Item] from the given values. - const Item({ - this.startTime, - this.endTime, - this.type, - this.content, - this.vocabularyFilterMatch, - this.speaker, - this.confidence, - this.stable, - }); - - /// Creates an [Item] from the given [Map]. - factory Item.fromMap(Map map) { - return Item( - startTime: map['StartTime'] != null ? map['StartTime'] as num : null, - endTime: map['EndTime'] != null ? map['EndTime'] as num : null, - type: map['Type'] != null - ? ItemType.fromValue(map['Type'] as String) - : null, - content: map['Content'] != null ? map['Content'] as String : null, - vocabularyFilterMatch: map['VocabularyFilterMatch'] != null - ? map['VocabularyFilterMatch'] as bool - : null, - speaker: map['Speaker'] != null ? map['Speaker'] as String : null, - confidence: - map['Confidence'] != null ? map['Confidence'] as double : null, - stable: map['Stable'] != null ? map['Stable'] as bool : null, - ); - } - - /// Creates an [Item] from the given JSON string. - factory Item.fromJson(String source) => - Item.fromMap(json.decode(source) as Map); - - /// Returns the [Map] representation of this [Item]. - Map toMap() { - return { - 'StartTime': startTime, - 'EndTime': endTime, - 'Type': type?.value, - 'Content': content, - 'VocabularyFilterMatch': vocabularyFilterMatch, - 'Speaker': speaker, - 'Confidence': confidence, - 'Stable': stable, - }; - } - - /// Returns the JSON string representation of this [Item]. - String toJson() => json.encode(toMap()); -} - -/// The type of [Item] identified in a transcription. -enum ItemType { - pronunciation('pronunciation'), - punctuation('punctuation'); - - /// Creates an [ItemType] with the given value. - const ItemType(this.value); - - /// The item type value. - final String value; - - /// Returns the [ItemType] for the given value. - factory ItemType.fromValue(String value) { - return ItemType.values.firstWhere((e) => e.value == value); - } -} - -/// Contains entities identified as personally identifiable information (PII) -/// in your transcription output, along with various associated attributes. -/// -/// Examples include category, confidence score, type, stability score, -/// and start and end times. -final class Entity { - /// The start time, in milliseconds, of the utterance that was identified - /// as PII. - final num? startTime; - - /// The end time, in milliseconds, of the utterance that was identified - /// as PII. - final num? endTime; - - /// The category of information identified. The only category is `PII`. - final String? category; - - /// The type of PII identified. For example, `NAME` or `CREDIT_DEBIT_NUMBER`. - final String? type; - - /// The word or words identified as PII. - final String? content; - - /// The confidence score associated with the identified PII entity in audio. - /// - /// Confidence scores are values between 0 and 1. A larger value indicates - /// a higher probability that the identified entity correctly matches - /// the entity spoken in your media. - final double? confidence; - - /// Creates an [Entity] from the given values. - const Entity({ - this.startTime, - this.endTime, - this.category, - this.type, - this.content, - this.confidence, - }); - - /// Creates an [Entity] from the given [Map]. - factory Entity.fromMap(Map map) { - return Entity( - startTime: map['StartTime'] != null ? map['StartTime'] as num : null, - endTime: map['EndTime'] != null ? map['EndTime'] as num : null, - category: map['Category'] != null ? map['Category'] as String : null, - type: map['Type'] != null ? map['Type'] as String : null, - content: map['Content'] != null ? map['Content'] as String : null, - confidence: - map['Confidence'] != null ? map['Confidence'] as double : null, - ); - } - - /// Creates an [Entity] from the given JSON string. - factory Entity.fromJson(String source) => - Entity.fromMap(json.decode(source) as Map); - - /// Returns the [Map] representation of this [Entity]. - Map toMap() { - return { - 'StartTime': startTime, - 'EndTime': endTime, - 'Category': category, - 'Type': type, - 'Content': content, - 'Confidence': confidence, - }; - } - - /// Returns the JSON string representation of this [Entity]. - String toJson() => json.encode(toMap()); -} diff --git a/team_b/yappy/lib/services/offline_model.dart b/team_b/yappy/lib/services/offline_model.dart deleted file mode 100644 index 32eec9cd..00000000 --- a/team_b/yappy/lib/services/offline_model.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:sherpa_onnx/sherpa_onnx.dart' as sherpa_onnx; -import 'model_manager.dart'; - -Future getOfflineModelConfig( - {required int type}) async { - final modelManager = ModelManager(); - - switch (type) { - case 0: - final modelType = await modelManager.getModelTypeString('offline') ?? 'whisper'; - - return sherpa_onnx.OfflineModelConfig( - whisper: sherpa_onnx.OfflineWhisperModelConfig( - encoder: await modelManager.getModelPath('offline', 'offline_encoder.onnx'), - decoder: await modelManager.getModelPath('offline', 'offline_decoder.onnx'), - ), - tokens: await modelManager.getModelPath('offline', 'offline_tokens.txt'), - modelType: modelType, - debug: false, - numThreads: 1 - ); - default: - throw ArgumentError('Unsupported type: $type'); - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/services/online_model.dart b/team_b/yappy/lib/services/online_model.dart deleted file mode 100644 index e2c9ddd0..00000000 --- a/team_b/yappy/lib/services/online_model.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:sherpa_onnx/sherpa_onnx.dart' as sherpa_onnx; -import 'model_manager.dart'; - -Future getOnlineModelConfig( - {required int type}) async { - final modelManager = ModelManager(); - - switch (type) { - case 0: - final modelType = await modelManager.getModelTypeString('online') ?? 'zipformer'; - - return sherpa_onnx.OnlineModelConfig( - transducer: sherpa_onnx.OnlineTransducerModelConfig( - encoder: await modelManager.getModelPath('online', 'online_encoder.onnx'), - decoder: await modelManager.getModelPath('online', 'online_decoder.onnx'), - joiner: await modelManager.getModelPath('online', 'online_joiner.onnx'), - ), - tokens: await modelManager.getModelPath('online', 'online_tokens.txt'), - modelType: modelType, - ); - default: - throw ArgumentError('Unsupported type: $type'); - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/services/openai_helper.dart b/team_b/yappy/lib/services/openai_helper.dart deleted file mode 100644 index d2fb29d4..00000000 --- a/team_b/yappy/lib/services/openai_helper.dart +++ /dev/null @@ -1,429 +0,0 @@ -import 'dart:convert'; - -import 'package:dart_openai/dart_openai.dart'; -import 'package:flutter/foundation.dart'; -import 'package:http/http.dart' as http; -import 'package:yappy/main.dart'; -import 'package:yappy/services/database_helper.dart'; -import 'package:yappy/services/file_handler.dart'; - -class OpenAIHelper { - final List> messages = []; - final currentOpenAIModel = "gpt-4o-mini"; // Ensure this is a current and valid model - - final String restaurantContextPrompt = - '''You are a restaurant assistant. Take the following audio transcript of a waiter taking patrons' orders and generate a summary of patrons' orders at a restaurant. You must separate out different speakers' orders and refer to them using using "Seat 1", "Seat 2", "Seat 3", etc. - - Respond in plain text. - - Example format: - - Seat 1: Hamburger, hold the lettuce, fries, Diet Dr. Pepper. - - Seat 2: Caesar salad, iced tea. - - Audio Transcript: - '''; - - final String mechanicContextPrompt = - '''You are a vehicle mechanic assistant. Take the following audio transcript of a customer describing their vehicle's issues and generate a summary of vehicle's issues and include suggestions for resolution. - - Respond in plain text. - - Example format: - - Customer: My car is making a weird whirring noise at idle. It is also leaking oil. - - Audio Transcript: - '''; - - final String medicalContextPrompt = - '''You are a medical assistant. Take the following audio transcript of a physician discussing a patient's concerns and generate a summary of patient's issues and include suggestions for resolution. - - Respond in plain text. - - Example format: - - Patient: I've been feeling really itchy recently. And I've been having trouble sleeping. - - Physician: I see. I recommend you take an antihistamine for the itching and try to avoid caffeine in the evenings. - - Audio Transcript: - '''; - - Future summarizeTranscription(int userId, String industry, int transcriptId) async { - // Pulls the transcript text from the database - Map? transcript = await dbHelper.getTranscriptById(transcriptId); - if (transcript == null) { - throw Exception('Transcript with given ID not found'); - } - - String contextPrompt = ""; - switch (industry) { - case "Restaurant": - contextPrompt = restaurantContextPrompt; - break; - case "Medical Doctor" || "Medical Patient": - contextPrompt = medicalContextPrompt; - break; - case "Vehicle Maintenance": - contextPrompt = mechanicContextPrompt; - break; - } - - String fullPromptToSend = contextPrompt + transcript['transcript_text_data']; - - List messages = [ - OpenAIChatCompletionChoiceMessageModel( - role: OpenAIChatMessageRole.user, content: [OpenAIChatCompletionChoiceMessageContentItemModel.text(fullPromptToSend)]) - ]; - - try { - final completion = await OpenAI.instance.chat - .create(model: currentOpenAIModel, messages: messages); - // Adds the AI response to the previously saved transcript in the database - await dbHelper.saveTranscriptAiResponse(userId: userId, - transcriptId: transcriptId, - text: transcript['transcript_text_data'], - aiResponse: completion.choices[0].message.content.toString(), industry: industry); - return completion.choices[0].message.content.toString(); - } catch (e) { - rethrow; - } - } - - Future startTranscriptChatAssistant(String userQuery, String industry) async { - - /** - * Chat assistant setup works as follows: - * 1. Upload a file to OpenAI - * 2. Create a vector store - * 3. Attach the file to the vector store - * 4. Create an assistant - * 5. Create a thread for the assistant to use - */ - - FileHandler fileHandler = FileHandler(); - OpenAIHelper openAIHelper = OpenAIHelper(); - DatabaseHelper dbHelper = DatabaseHelper(); - String? response = ""; - String? apiKey = preferences.getString('openai_api_key'); - - // Pulls all transcripts for the current industry - List> transcripts = await dbHelper.getAllTranscriptsByIndustry(industry); - // Saves all industry transcripts to local storage - List localTranscriptFileNames = []; - for (var transcript in transcripts) { - String currentFileName = await fileHandler.saveTranscriptTextToLocal(dbHelper, transcript['transcript_id']); - localTranscriptFileNames.add(currentFileName); - } - - List transcriptPaths = []; - for (String transcriptId in localTranscriptFileNames) { - String path = '${await fileHandler.localStoragePath}/$transcriptId'; - transcriptPaths.add(path); - } - - List? fileIds = await openAIHelper.uploadFile(transcriptPaths, apiKey); - String? vectorStoreId = "", assistantId = "", threadId = ""; - - bool successfulSetup = false; - if (fileIds != null) { - vectorStoreId = await openAIHelper.createVectorStore(apiKey); - if (vectorStoreId != null) { - // Attach the files to the vector store - bool attached = await openAIHelper.attachFilesToVectorStore(vectorStoreId, fileIds, apiKey); - if (attached) { - // Create an Assistant - assistantId = await createOpenAIAssistant(vectorStoreId, fileIds, apiKey); - if (assistantId != null) { - // Create a Thread - threadId = await createNewThreadForAssistant(apiKey); - if (threadId != null) { - debugPrint("✅ All chat setup steps completed successfully!"); - successfulSetup = true; - } - } - } - } - } - - /** - * Chat assistant run works as follows: - * 1. Send the user message to the created thread - * 2. Run the assistant on the thread - * 3. Query the current vector store using the assistant for information based on the user's input question - * 4. Polls for results and returns them to the user as a response - */ - if (successfulSetup) { - await openAIHelper.sendMessageToThread(threadId!, userQuery, apiKey); - String? runId = await openAIHelper.runAssistantOnThread(threadId, assistantId!, apiKey!); - if (runId != null) { - debugPrint("🚀 Polling in background for Yappy assistant response!"); - response = await openAIHelper.getAssistantResponseInBackground(threadId, runId, apiKey); - } - } - - // Clean up local storage after assistant processing has completed - for (String currentFileName in localTranscriptFileNames) { - await fileHandler.deleteFile(currentFileName); - } - - return response; - } - - Future?> uploadFile(List filePaths, apiKey) async { - var url = Uri.parse("https://api.openai.com/v1/files"); - - List fileIds = []; - for (String filePath in filePaths) { - var request = http.MultipartRequest("POST", url) - ..headers["Authorization"] = "Bearer $apiKey" - ..headers["OpenAI-Beta"] = "assistants=v2" - ..fields["purpose"] = "assistants" - ..files.add(await http.MultipartFile.fromPath("file", filePath)); - - var response = await request.send(); - var responseBody = await response.stream.bytesToString(); - var jsonResponse = jsonDecode(responseBody); - - if (response.statusCode == 200) { - debugPrint("✅ File uploaded: ${jsonResponse["id"]}"); - fileIds.add(jsonResponse["id"]); - } else { - debugPrint("❌ File upload failed: $responseBody"); - return null; - } - } - return fileIds; - } - - Future createVectorStore(apiKey) async { - var url = Uri.parse("https://api.openai.com/v1/vector_stores"); - - var response = await http.post( - url, - headers: { - "Authorization": "Bearer $apiKey", - "Content-Type": "application/json" - }, - body: jsonEncode({"name": "Transcript Vector Store"}), - ); - var jsonResponse = jsonDecode(response.body); - - if (response.statusCode == 200) { - debugPrint("✅ Vector store created: ${jsonResponse["id"]}"); - return jsonResponse["id"]; - } else { - debugPrint("❌ Vector store creation failed: ${response.body}"); - return null; - } - } - - Future attachFilesToVectorStore(String vectorStoreId, List fileIds, apiKey) async { - var url = Uri.parse("https://api.openai.com/v1/vector_stores/$vectorStoreId/files"); - - for (String fileId in fileIds) { - var response = await http.post( - url, - headers: { - "Authorization": "Bearer $apiKey", - "Content-Type": "application/json", - }, - body: jsonEncode({"file_id": fileId}), - ); - - if (response.statusCode != 200) { - debugPrint("❌ Attaching file failed: ${response.body}"); - return false; - } else { - debugPrint("✅ File with ID $fileId attached to vector store."); - } - } - return true; - } - - Future createOpenAIAssistant(String vectorStoreId, List fileIds, apiKey) async { - var url = Uri.parse("https://api.openai.com/v1/assistants"); - - var response = await http.post( - url, - headers: { - "Authorization": "Bearer $apiKey", - "Content-Type": "application/json", - "OpenAI-Beta" : "assistants=v2" - }, - body: jsonEncode({ - "name": "Yappy", - "instructions": "You can retrieve transcript information from a vector store. No need to cite sources, and please use complete sentences rather then bullet points. Respond in plain text.", - "model": currentOpenAIModel, - "tools": [ - {"type": "file_search"}, - {"type": "code_interpreter"}, - ], - "tool_resources": { - "file_search": { - "vector_store_ids": [vectorStoreId] - }, - "code_interpreter": { - "file_ids": fileIds - } - } - }), - ); - - if (response.statusCode == 200) { - var jsonResponse = jsonDecode(response.body); - debugPrint("✅ Assistant created: ${jsonResponse["id"]}"); - return jsonResponse["id"]; // Return Assistant ID - } else { - debugPrint("❌ Assistant creation failed: ${response.body}"); - return null; - } - } - - Future createNewThreadForAssistant(apiKey) async { - var url = Uri.parse("https://api.openai.com/v1/threads"); - - var response = await http.post( - url, - headers: { - "Authorization": "Bearer $apiKey", - "Content-Type": "application/json", - "OpenAI-Beta" : "assistants=v2" - }, - body: jsonEncode({}), - ); - - if (response.statusCode == 200) { - var jsonResponse = jsonDecode(response.body); - debugPrint("✅ Thread created: ${jsonResponse["id"]}"); - return jsonResponse["id"]; - } else { - debugPrint("❌ Thread creation failed: ${response.body}"); - return null; - } - } - - Future sendMessageToThread(String threadId, String message, apiKey) async { - var url = Uri.parse("https://api.openai.com/v1/threads/$threadId/messages"); - - var response = await http.post( - url, - headers: { - "Authorization": "Bearer $apiKey", - "Content-Type": "application/json", - "OpenAI-Beta" : "assistants=v2" - }, - body: jsonEncode({ - "role": "user", - "content": message - }), - ); - - return response.statusCode == 200; - } - - Future runAssistantOnThread(String threadId, String assistantId, apiKey) async { - var url = Uri.parse("https://api.openai.com/v1/threads/$threadId/runs"); - - var response = await http.post( - url, - headers: { - "Authorization": "Bearer $apiKey", - "Content-Type": "application/json", - "OpenAI-Beta" : "assistants=v2" - }, - body: jsonEncode({ - "assistant_id": assistantId - }), - ); - - if (response.statusCode == 200) { - var jsonResponse = jsonDecode(response.body); - debugPrint("✅ Run created: ${jsonResponse["id"]}"); - return jsonResponse["id"]; - } else { - debugPrint("❌ Failed to run assistant: ${response.body}"); - return null; - } - } - - Future getAssistantResponseInBackground(String threadId, String runId, String apiKey) async { - return compute(getAssistantResponseIsolate, {"threadId": threadId, "runId": runId, "apiKey": apiKey}); - } - - Future getAssistantResponseIsolate(Map params) async { - String threadId = params["threadId"]!; - String runId = params["runId"]!; - String apiKey = params["apiKey"]!; - - return await getAssistantResponse(threadId, runId, apiKey); - } - - Future getAssistantResponse(String threadId, String runId, apiKey) async { - var url = Uri.parse("https://api.openai.com/v1/threads/$threadId/runs/$runId"); - int maxAttempts = 5; // Limit retries - int attempt = 0; - - while (attempt < maxAttempts) { - // Poll at an interval => on separate thread to prevent blocking the UI - await Future.delayed(Duration(seconds: 3)); - attempt++; - - var response = await http.get(url, headers: { - "Authorization": "Bearer $apiKey", - "Content-Type": "application/json", - "OpenAI-Beta" : "assistants=v2" - }); - - var jsonResponse = jsonDecode(utf8.decode(response.bodyBytes)); - String status = jsonResponse["status"]; - - if (status == "completed") { - break; // Exit loop when assistant has finished processing - } else if (status == "failed" || status == "cancelled") { - debugPrint("❌ Assistant run failed: $jsonResponse"); - return null; - } - } - - // Fetch latest messages in the thread to get assistant's response - return getLatestAssistantMessage(threadId, apiKey); - } - - Future getLatestAssistantMessage(String threadId, apiKey) async { - debugPrint("Fetching latest assistant response on thread with ID $threadId"); - var url = Uri.parse("https://api.openai.com/v1/threads/$threadId/messages"); - - var response = await http.get(url, headers: { - "Authorization": "Bearer $apiKey", - "Content-Type": "application/json", - "OpenAI-Beta" : "assistants=v2" - }); - - if (response.statusCode == 200) { - var jsonResponse = jsonDecode(utf8.decode(response.bodyBytes)); - var messages = jsonResponse["data"]; - - for (var message in messages.reversed) { // Read messages from newest to oldest - if (message["role"] == "assistant") { - String textResponse = message["content"][0]["text"]["value"]; - return cleanResponse(textResponse); - } - } - } else { - debugPrint("❌ Failed to fetch messages: ${response.body}"); - } - - return null; - } - - // Helper function to remove unwanted characters - String cleanResponse(String response) { - // Remove source citations - response = response.replaceAll(RegExp(r'【\d+:\d+†source】'), '').trim(); - // Remove unwanted characters and return - return response.replaceAll(RegExp(r'[\u0000-\u001F\u007F-\u009F]'), '').trim(); - } -} diff --git a/team_b/yappy/lib/services/protocol.dart b/team_b/yappy/lib/services/protocol.dart deleted file mode 100644 index 88831e38..00000000 --- a/team_b/yappy/lib/services/protocol.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:http2/http2.dart'; - -import 'event_stream/header.dart'; -import 'event_stream/message.dart'; -import 'event_stream/message_signer.dart'; -import 'event_stream/stream_codec.dart'; - -/// Encodes [EventStreamMessage]s into [Uint8List]s. -final class EventStreamEncoder - extends Converter { - const EventStreamEncoder(); - - @override - Uint8List convert(EventStreamMessage input) => EventStreamCodec.encode(input); - - @override - Sink startChunkedConversion(Sink sink) => - _PacketConversionSink(sink, this); -} - -/// Encodes [Uint8List]s into [DataStreamMessage]s. -final class DataStreamMessageEncoder - extends Converter { - const DataStreamMessageEncoder(); - - @override - DataStreamMessage convert(Uint8List input) => DataStreamMessage(input); - - @override - Sink startChunkedConversion(Sink sink) => - _PacketConversionSink(sink, this); -} - -/// Encodes [Uint8List]s into [EventStreamMessage]s. -final class AudioEventEncoder extends Converter { - const AudioEventEncoder(); - - @override - EventStreamMessage convert(Uint8List input) => EventStreamMessage( - headers: [ - const EventStreamStringHeader( - ':content-type', - 'application/octet-stream', - ), - const EventStreamStringHeader(':event-type', 'AudioEvent'), - const EventStreamStringHeader(':message-type', 'event'), - ], - payload: input, - ); - - @override - Sink startChunkedConversion(Sink sink) => - _PacketConversionSink(sink, this); -} - -/// Signs [Uint8List]s into [EventStreamMessage]s. -final class AudioMessageSigner - extends Converter { - const AudioMessageSigner(this._messageSigner); - - final EventStreamMessageSigner _messageSigner; - - @override - EventStreamMessage convert(Uint8List input) => _messageSigner.sign(input); - - @override - Sink startChunkedConversion(Sink sink) => - _PacketConversionSink(sink, this); -} - -/// Joins/splits [Uint8List]s into [Uint8List]s of a fixed size. -final class AudioDataChunker extends Converter { - const AudioDataChunker(this.chunkSize) : assert(chunkSize >= 0); - - /// The size of the chunks to split the audio data into. - /// Zero value disables chunking. - final int chunkSize; - - @override - Uint8List convert(Uint8List input) { - return input; - } - - @override - Sink startChunkedConversion(Sink sink) => - _AudioDataChunkedConversionSink(sink, chunkSize); -} - -final class _AudioDataChunkedConversionSink - implements ChunkedConversionSink { - _AudioDataChunkedConversionSink(this.sink, this._chunkSize) - : assert(_chunkSize > 0), - _buffer = Uint8List(_chunkSize); - - final Sink sink; - final int _chunkSize; - final Uint8List _buffer; - - int _bufferSize = 0; - int _totalSize = 0; - - @override - void add(Uint8List chunk) { - final chunkLength = chunk.length; - - if (_chunkSize == 0) { - sink.add(chunk); - _totalSize += chunkLength; - return; - } - - int offset = 0; - - while (offset < chunkLength) { - final remaining = chunkLength - offset; - final remainingBuffer = _chunkSize - _bufferSize; - final copyLength = - remaining < remainingBuffer ? remaining : remainingBuffer; - _buffer.setRange(_bufferSize, _bufferSize + copyLength, chunk, offset); - _bufferSize += copyLength; - offset += copyLength; - - if (_bufferSize == _chunkSize) { - sink.add(_buffer); - _totalSize += _bufferSize; - _bufferSize = 0; - } - } - } - - @override - void close() { - if (_bufferSize > 0) { - sink.add(_buffer.sublist(0, _bufferSize)); - _totalSize += _bufferSize; - } - - if (_totalSize > 0) { - // Send an empty chunk to signal the end of the audio stream. - sink.add(Uint8List(0)); - } - - sink.close(); - } -} - -final class _PacketConversionSink implements ChunkedConversionSink { - const _PacketConversionSink(this.sink, this.converter); - - final Sink sink; - final Converter converter; - - @override - void add(S chunk) { - sink.add(converter.convert(chunk)); - } - - @override - void close() { - sink.close(); - } -} diff --git a/team_b/yappy/lib/services/restaurant_api_module.dart b/team_b/yappy/lib/services/restaurant_api_module.dart deleted file mode 100644 index 6e847bb6..00000000 --- a/team_b/yappy/lib/services/restaurant_api_module.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'database_helper.dart'; - -class RestaurantAPI { - final DatabaseHelper dbHelper = DatabaseHelper(); - - //Steps 1,2,3,4 needs to be done then in step 4 we will get extractedItems List - which contains exact orders - e.g[pizza,coke,chips] then that extractedItems - //needs to be passed to step 5 - - /// STEP 5: VALIDATE AGAINST THE RESTAURANT MENU - /// We get a list of items from the LLM (e.g. ["Margherita Pizza", "Coke"]). - /// We compare each item with what’s in the MenuItem table. - /// Only items that match are returned in `validItems`. - Future> validateMenuItems(List extractedItems) async { - // 1) Fetch the menu from the database (MenuItem table). - final db = await dbHelper.database; - final menuRows = - await db.query('MenuItem'); // Must contain 'item_name' column - - // Convert each item_name to lowercase for easy comparison - List menuNames = menuRows - .map((row) => (row['item_name'] as String).toLowerCase()) - .toList(); - - // 2) Check which extracted items exist in the menu - List validItems = []; - for (var item in extractedItems) { - for (var menuItem in menuNames) { - if (item.toLowerCase().contains(menuItem)) { - validItems.add(item); - break; - } - } - } - - return validItems; - } - - /// STEP 6: STORE THE VALIDATED ORDER IN THE DATABASE - /// Once we have a list of valid items, we store them in RestaurantOrder - /// with a "Pending" status. For simplicity, we join them into a single string - /// (e.g. "Margherita Pizza, Coke, Garlic Bread"). - Future storeValidatedOrder(List validItems) async { - if (validItems.isEmpty) { - debugPrint(" No valid items to store in database.Please chech whether MenuItem table and RestaurantOrder table has column or not.If not then create it"); - return; - } - - String orderText = validItems.join(", "); - debugPrint("Storing order: $orderText"); - - final db = await dbHelper.database; - await db.insert('RestaurantOrder', { - 'order_text': orderText, - 'order_status': 'Pending', - }); - - debugPrint("Order successfully stored in database."); - } - - /// STEP 7: UI FETCHES & DISPLAYS ORDER SUMMARY - /// - /// We provide a method to retrieve the latest orders from RestaurantOrder - /// so the UI can display them. - Future>> getOrderHistory() async { - final db = await dbHelper.database; - return await db.query('RestaurantOrder', orderBy: 'order_id DESC'); - } - - // ------------------------------------------------------------------- - // Optional convenience method that does Steps 5 & 6 in one shot: - // Given a list of extracted items, validate them, then store them. - // The UI can then call getOrderHistory to show it. - // ------------------------------------------------------------------- - Future processValidatedItems(List extractedItems) async { - // Step 5 - List validItems = await validateMenuItems(extractedItems); - - // Step 6 - await storeValidatedOrder(validItems); - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/services/speaker_model.dart b/team_b/yappy/lib/services/speaker_model.dart deleted file mode 100644 index dd59d212..00000000 --- a/team_b/yappy/lib/services/speaker_model.dart +++ /dev/null @@ -1,12 +0,0 @@ -import './model_manager.dart'; - -Future getSpeakerModel({required int type}) async { - final modelManager = ModelManager(); - - switch (type) { - case 0: - return await modelManager.getModelPath('speaker', 'speaker_model.onnx'); - default: - throw ArgumentError('Unsupported type: $type'); - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/services/speech_isolate.dart b/team_b/yappy/lib/services/speech_isolate.dart deleted file mode 100644 index fc22c1d7..00000000 --- a/team_b/yappy/lib/services/speech_isolate.dart +++ /dev/null @@ -1,327 +0,0 @@ -import 'dart:isolate'; -import 'dart:async'; -import 'package:flutter/foundation.dart'; -import 'package:sherpa_onnx/sherpa_onnx.dart' as sherpa_onnx; - -// Message to be sent to the isolate -class ProcessSegmentMessage { - final Float32List samples; - final int sampleRate; - final int segmentIndex; - final Map recognizerConfigs; - - ProcessSegmentMessage({ - required this.samples, - required this.sampleRate, - required this.segmentIndex, - required this.recognizerConfigs, - }); -} - -// Response from the isolate -class ProcessSegmentResult { - final int segmentIndex; - final String text; - final String speakerId; - final Float32List? embedding; - final bool success; - final String? error; - final int? newSpeakerCount; - - ProcessSegmentResult({ - required this.segmentIndex, - required this.text, - required this.speakerId, - this.embedding, - required this.success, - this.error, - this.newSpeakerCount, - }); -} - -class SpeechProcessingIsolate { - Isolate? _isolate; - SendPort? _sendPort; - final _receivePort = ReceivePort(); - final _responseCompleter = Completer(); - - // Stream controller for receiving results - final _resultController = StreamController.broadcast(); - Stream get results => _resultController.stream; - - // Keep track of speaker count locally in the isolate handler - int _currentSpeakerCount = 0; - - // Initialize the isolate and recognizers - Future initialize(Map configs) async { - // Check if already initialized - if (_isolate != null) return; - - // Start the isolate - _isolate = await Isolate.spawn( - _isolateEntry, - [_receivePort.sendPort, configs] - ); - - // Set up communication - _receivePort.listen((message) { - if (message is SendPort) { - // First message is the SendPort for communicating with the isolate - _sendPort = message; - _responseCompleter.complete(message); - } else if (message is ProcessSegmentResult) { - // Subsequent messages are results from the isolate - - // Debug the speaker ID - debugPrint("Received result from isolate - Speaker ID: ${message.speakerId}, Count: ${message.newSpeakerCount}"); - - // Update local speaker count if needed - if (message.newSpeakerCount != null && message.newSpeakerCount! > _currentSpeakerCount) { - _currentSpeakerCount = message.newSpeakerCount!; - debugPrint("Updated local speaker count to: $_currentSpeakerCount"); - } - - _resultController.add(message); - } - }); - - // Wait until communication is established - await _responseCompleter.future; - } - - // Process a segment asynchronously - Future processSegment(ProcessSegmentMessage message) async { - if (_sendPort == null) { - throw Exception('Isolate not initialized'); - } - - // Update our local copy of speaker count if the incoming count is higher - if (message.recognizerConfigs['currentSpeakerCount'] != null) { - int incomingSpeakerCount = message.recognizerConfigs['currentSpeakerCount']; - if (incomingSpeakerCount > _currentSpeakerCount) { - _currentSpeakerCount = incomingSpeakerCount; - debugPrint("Updated isolate speaker count to: $_currentSpeakerCount"); - } - } - - // Send the message to isolate with updated speaker count - final updatedMessage = ProcessSegmentMessage( - samples: message.samples, - sampleRate: message.sampleRate, - segmentIndex: message.segmentIndex, - recognizerConfigs: { - ...message.recognizerConfigs, - 'currentSpeakerCount': _currentSpeakerCount, - }, - ); - - _sendPort!.send(updatedMessage); - } - - // Update the speaker count (call this when a new speaker is detected in the main thread) - void updateSpeakerCount(int count) { - if (count > _currentSpeakerCount) { - _currentSpeakerCount = count; - debugPrint("Manually updated isolate speaker count to: $_currentSpeakerCount"); - } - } - - // Dispose resources - void dispose() { - _isolate?.kill(priority: Isolate.immediate); - _isolate = null; - _sendPort = null; - _receivePort.close(); - _resultController.close(); - } - - // Static entry point for the isolate - static void _isolateEntry(List args) { - final SendPort sendPort = args[0]; - final configs = args[1] as Map; - - final receivePort = ReceivePort(); - sendPort.send(receivePort.sendPort); - - // Initialize recognizers - sherpa_onnx.initBindings(); - sherpa_onnx.OfflineRecognizer? offlineRecognizer; - sherpa_onnx.SpeakerEmbeddingExtractor? speakerExtractor; - sherpa_onnx.SpeakerEmbeddingManager? speakerManager; - - // Track speaker count inside the isolate - int isolateSpeakerCount = 0; - - _initializeRecognizers(configs).then((initialized) { - if (initialized != null) { - offlineRecognizer = initialized['offlineRecognizer']; - speakerExtractor = initialized['speakerExtractor']; - speakerManager = initialized['speakerManager']; - - receivePort.listen((message) { - if (message is ProcessSegmentMessage) { - // Update speaker count from message if available - if (message.recognizerConfigs['currentSpeakerCount'] != null) { - int msgCount = message.recognizerConfigs['currentSpeakerCount']; - if (msgCount > isolateSpeakerCount) { - isolateSpeakerCount = msgCount; - debugPrint("Isolate updated count to: $isolateSpeakerCount"); - } - } - - _processSegment( - message, - offlineRecognizer!, - speakerExtractor!, - speakerManager!, - sendPort, - isolateSpeakerCount, - (newCount) { - isolateSpeakerCount = newCount; - debugPrint("Callback updated isolate count to: $isolateSpeakerCount"); - } - ); - } - }); - } - }); - } - - // Initialize recognizers inside the isolate - static Future?> _initializeRecognizers(Map configs) async { - try { - // Create offline recognizer - final offlineConfig = sherpa_onnx.OfflineRecognizerConfig( - model: configs['offlineModelConfig'] - ); - final offlineRecognizer = sherpa_onnx.OfflineRecognizer(offlineConfig); - - // Create speaker extractor - final speakerConfig = sherpa_onnx.SpeakerEmbeddingExtractorConfig( - model: configs['speakerModel'], - numThreads: 2, - debug: false, - provider: 'cpu', - ); - final speakerExtractor = sherpa_onnx.SpeakerEmbeddingExtractor(config: speakerConfig); - - // Create speaker manager - final speakerManager = sherpa_onnx.SpeakerEmbeddingManager(speakerExtractor.dim); - - return { - 'offlineRecognizer': offlineRecognizer, - 'speakerExtractor': speakerExtractor, - 'speakerManager': speakerManager, - }; - } catch (e) { - debugPrint('Error initializing recognizers in isolate: $e'); - return null; - } - } - - // Process a segment inside the isolate - static Future _processSegment( - ProcessSegmentMessage message, - sherpa_onnx.OfflineRecognizer offlineRecognizer, - sherpa_onnx.SpeakerEmbeddingExtractor speakerExtractor, - sherpa_onnx.SpeakerEmbeddingManager speakerManager, - SendPort sendPort, - int currentSpeakerCount, - Function(int) updateSpeakerCount - ) async { - try { - debugPrint('Isolate: Processing segment ${message.segmentIndex} (${message.samples.length} samples)'); - - if (message.samples.isEmpty) { - sendPort.send(ProcessSegmentResult( - segmentIndex: message.segmentIndex, - text: '', - speakerId: 'Unknown', - success: false, - error: 'Empty samples', - )); - return; - } - - // Perform offline speech recognition - final offlineStream = offlineRecognizer.createStream(); - offlineStream.acceptWaveform( - samples: message.samples, - sampleRate: message.sampleRate - ); - - offlineRecognizer.decode(offlineStream); - final result = offlineRecognizer.getResult(offlineStream); - - debugPrint('Isolate: Recognition result: "${result.text}"'); - - // Skip speaker identification if result is empty - if (result.text.trim().isEmpty || result.text.trim().startsWith(RegExp(r'[\[\(]'))) { - sendPort.send(ProcessSegmentResult( - segmentIndex: message.segmentIndex, - text: result.text, - speakerId: 'Unknown', - success: true, - )); - - offlineStream.free(); - return; - } - - // Speaker identification - final speakerStream = speakerExtractor.createStream(); - speakerStream.acceptWaveform( - samples: message.samples, - sampleRate: message.sampleRate, - ); - - speakerStream.inputFinished(); - final embedding = speakerExtractor.compute(speakerStream); - - // Search for matching speaker - // Adjust threshold lower for better accuracy - final threshold = 0.4; - var speakerId = speakerManager.search(embedding: embedding, threshold: threshold); - - int newSpeakerCount = currentSpeakerCount; - - // If no match, register a new speaker - if (speakerId.isEmpty) { - // Increment speaker count for the new speaker - newSpeakerCount = currentSpeakerCount + 1; - - speakerId = 'Speaker $newSpeakerCount'; - debugPrint('Isolate: New speaker detected: $speakerId (count: $newSpeakerCount)'); - speakerManager.add(name: speakerId, embedding: embedding); - - // Update the callback - updateSpeakerCount(newSpeakerCount); - } else { - debugPrint('Isolate: Matched existing speaker: $speakerId'); - } - - // Send the result back with updated speaker count - sendPort.send(ProcessSegmentResult( - segmentIndex: message.segmentIndex, - text: result.text, - speakerId: speakerId, - embedding: embedding, - success: true, - newSpeakerCount: newSpeakerCount, - )); - - // Clean up - offlineStream.free(); - speakerStream.free(); - } catch (e) { - debugPrint('Isolate: Error processing segment: $e'); - sendPort.send(ProcessSegmentResult( - segmentIndex: message.segmentIndex, - text: '', - speakerId: 'Unknown', - success: false, - error: e.toString(), - )); - } - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/services/speech_state.dart b/team_b/yappy/lib/services/speech_state.dart deleted file mode 100644 index 62256b57..00000000 --- a/team_b/yappy/lib/services/speech_state.dart +++ /dev/null @@ -1,1000 +0,0 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:io'; -import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:record/record.dart'; -import 'package:sherpa_onnx/sherpa_onnx.dart' as sherpa_onnx; -import '../main.dart'; -import 'utils.dart'; -import 'online_model.dart'; -import 'offline_model.dart'; -import 'speaker_model.dart'; -import 'vad_model.dart'; -import 'speech_isolate.dart'; -import 'package:aws_common/aws_common.dart'; -import 'client.dart'; -import 'models.dart'; - -Future createOnlineRecognizer() async { - final type = 0; - - final modelConfig = await getOnlineModelConfig(type: type); - final config = sherpa_onnx.OnlineRecognizerConfig( - model: modelConfig, - ruleFsts: '', - ); - - return sherpa_onnx.OnlineRecognizer(config); -} - -Future createOfflineRecognizer() async { - final type = 0; - - final modelConfig = await getOfflineModelConfig(type: type); - final config = sherpa_onnx.OfflineRecognizerConfig( - model: modelConfig - ); - - return sherpa_onnx.OfflineRecognizer(config); -} - -Future createSpeakerExtractor() async { - final type = 0; - - final model = await getSpeakerModel(type: type); - final config = sherpa_onnx.SpeakerEmbeddingExtractorConfig( - model: model, - numThreads: 2, - debug: false, - provider: 'cpu', - ); - - return sherpa_onnx.SpeakerEmbeddingExtractor(config: config); -} - -Future createVoiceActivityDetector() async { - final type = 0; - - final model = await getVadModel(type: type); - final sileroConfig = sherpa_onnx.SileroVadModelConfig( - model: model, - threshold: 0.5, - minSilenceDuration: 0.25, - minSpeechDuration: 0.1, - windowSize: 512, - maxSpeechDuration: 10.0, - ); - - final vadConfig = sherpa_onnx.VadModelConfig( - sileroVad: sileroConfig, - numThreads: 2, - provider: 'cpu', - debug: false, - ); - - return sherpa_onnx.VoiceActivityDetector(config: vadConfig, bufferSizeInSeconds: 10); -} - -class Conversation { - final List segments; - final String audioFilePath; - String awsTranscription = ''; - - Conversation({ - required this.segments, - required this.audioFilePath, - this.awsTranscription = '', - }); - - // Update JSON methods - Map toJson() => { - 'segments': segments.map((s) => s.toJson()).toList(), - 'audioFilePath': audioFilePath, - 'awsTranscription': awsTranscription, - }; - - factory Conversation.fromJson(Map json) => Conversation( - segments: (json['segments'] as List) - .map((s) => RecognizedSegment.fromJson(s)) - .toList(), - audioFilePath: json['audioFilePath'], - awsTranscription: json['awsTranscription'] ?? '', - ); - - String getTranscript({bool includeSpeakerTags = true}) { - final buffer = StringBuffer(); - RecognizedSegment? lastSegment; - - for (final segment in segments) { - if (segment.text.isEmpty) continue; - - if (buffer.isNotEmpty) { - if (lastSegment == null || lastSegment.speakerId != segment.speakerId) { - buffer.write('\n\n'); - } else { - buffer.write('\n'); - } - } - - if (includeSpeakerTags && segment.speakerId != null) { - buffer.write('${segment.speakerId}: '); - } - - buffer.write(segment.text); - lastSegment = segment; - } - - return buffer.toString(); - } - - String getAwsTranscript() { - return awsTranscription; - } -} - -class RecognizedSegment { - final int index; - String text; - String? speakerId; - bool isProcessed; - double start; - double end; - Float32List? speakerEmbedding; - - RecognizedSegment({ - required this.index, - required this.text, - this.speakerId, - this.isProcessed = false, - required this.start, - required this.end, - this.speakerEmbedding, - }); - - // Add a method to convert to/from JSON for persistence - Map toJson() => { - 'index': index, - 'text': text, - 'speakerId': speakerId, - 'isProcessed': isProcessed, - 'start': start, - 'end': end, - }; - - factory RecognizedSegment.fromJson(Map json) => RecognizedSegment( - index: json['index'], - text: json['text'], - speakerId: json['speakerId'], - isProcessed: json['isProcessed'], - start: json['start'], - end: json['end'], - ); -} - -class AudioSegment { - final Float32List samples; - final int sampleRate; - final int index; - final double start; - final double end; - - AudioSegment({ - required this.samples, - required this.sampleRate, - required this.index, - required this.start, - required this.end, - }); -} - -class SpeechState extends ChangeNotifier { - TranscribeStreamingClient? awsClient; - StreamSink? awsAudioStreamSink; - StreamSubscription? awsTranscriptSubscription; - String currentAwsTranscript = ''; - bool isAwsTranscribing = false; - StringBuffer awsTranscriptBuffer = StringBuffer(); - - final TextEditingController controller = TextEditingController(); - final AudioRecorder audioRecorder = AudioRecorder(); - - ValueNotifier> audioSamplesNotifier = ValueNotifier>([]); - - RecordState recordState = RecordState.stop; - bool isInitialized = false; - int currentIndex = 0; - double currentTimestamp = 0.0; - int currentSpeakerCount = 0; - - void updateAudioSamples(List newSamples) { - audioSamplesNotifier.value = newSamples; - } - // Store all recognized segments - final List recognizedSegments = []; - - // First pass - streaming recognition - sherpa_onnx.OnlineRecognizer? onlineRecognizer; - sherpa_onnx.OnlineStream? onlineStream; - - // Second pass - offline processing - SpeechProcessingIsolate? speechIsolate; - - // Voice Activity Detector - sherpa_onnx.VoiceActivityDetector? vad; - - List allAudioSamples = []; - // Buffer for collecting samples between endpoints - List currentSegmentSamples = []; - final int sampleRate = 16000; - - // Store segments that need offline processing - List pendingSegments = []; - bool isProcessingOffline = false; - - // Path to save the complete audio recording - String? recordingFilePath; - - // Create a Conversation object when recording is stopped - Conversation? lastConversation; - - Future initialize() async { - if (!isInitialized) { - try { - // init online recognizer - sherpa_onnx.initBindings(); - onlineRecognizer = await createOnlineRecognizer(); - onlineStream = onlineRecognizer?.createStream(); - - // init vad - vad = await createVoiceActivityDetector(); - - // Initialize the isolate with configuration - speechIsolate = SpeechProcessingIsolate(); - final offlineModelConfig = await getOfflineModelConfig(type: 0); - final speakerModel = await getSpeakerModel(type: 0); - - await speechIsolate?.initialize({ - 'offlineModelConfig': offlineModelConfig, - 'speakerModel': speakerModel, - }); - - // Set up listener for results from the isolate - speechIsolate?.results.listen((result) { - debugPrint("Received result from isolate: ${result.segmentIndex}, Speaker: ${result.speakerId}"); - - // Update speaker count if a new speaker was detected - if (result.newSpeakerCount != null && result.newSpeakerCount! > currentSpeakerCount) { - currentSpeakerCount = result.newSpeakerCount!; - debugPrint('Updated main thread speaker count to: $currentSpeakerCount'); - } - - // Ignore empty results - if (result.success && result.text.trim().isNotEmpty) { - // Make sure speakerId is not null or empty - String speakerId = result.speakerId; - if (speakerId.isEmpty) { - speakerId = 'Speaker Unknown'; - } - - _updateRecognizedSegment( - result.segmentIndex, - result.text, - speakerId: speakerId, - embedding: result.embedding, - ); - _updateDisplayText(); - } - }); - - isInitialized = true; - notifyListeners(); - } catch (e) { - debugPrint('Sherpa initialization failed: $e'); - } - } - } - - // Initialize AWS credentials and client - Future initializeAwsClient() async { - if (awsClient == null) { - try { - // Use StaticCredentialsProvider for simplicity - // In production, consider using more secure credential providers - final credentialsProvider = StaticCredentialsProvider( - AWSCredentials( - preferences.getString('aws_access_key')!, // Replace with your idkey - preferences.getString('aws_secret_key')! // Replace with your secretkey - ), - ); - - // Create AWS Transcribe client - awsClient = TranscribeStreamingClient( - region: preferences.getString('aws_region')!, // Replace with your AWS region - credentialsProvider: credentialsProvider, - ); - - await preferences.setBool('is_aws_available', true); - debugPrint('🚣 AWS Transcribe client initialized'); - } catch (e) { - await preferences.setBool('is_aws_available', false); - debugPrint('🚣 Error initializing AWS client: $e'); - } - } - } - - // Start AWS transcription - Future startAwsTranscription() async { - if (awsClient == null) { - await initializeAwsClient(); - } - - try { - isAwsTranscribing = true; - currentAwsTranscript = ''; - awsTranscriptBuffer.clear(); - - // Create request with proper parameters - final request = StartStreamTranscriptionRequest( - languageCode: LanguageCode.enUs, - mediaSampleRateHertz: sampleRate, - mediaEncoding: MediaEncoding.pcm, - showSpeakerLabel: true, - // Disable partial results stabilization for better accuracy - enablePartialResultsStabilization: false, - ); - - debugPrint('🚣 Starting AWS transcription with sample rate $sampleRate'); - - // Start streaming - final (response, sink, stream) = await awsClient!.startStreamTranscription(request); - awsAudioStreamSink = sink; - - // Create a transformer that converts TranscriptEvent to String - final stringStream = stream.transform( - StreamTransformer.fromHandlers( - handleData: (event, sink) { - if (event.transcript != null) { - try { - final transcript = _processTranscriptEvent(event); - if (transcript.isNotEmpty) { - sink.add(transcript); - } - } catch (e) { - debugPrint('🚣 Error processing transcript: $e'); - } - } - }, - handleError: (error, stackTrace, sink) { - debugPrint('🚣 AWS Transcription Stream Error: $error'); - sink.addError(error, stackTrace); - }, - handleDone: (sink) { - debugPrint('🚣 AWS Transcription Stream Done'); - sink.close(); - }, - ) - ); - - awsTranscriptSubscription = stringStream.listen( - (transcriptText) { - if (transcriptText.isNotEmpty) { - currentAwsTranscript = transcriptText; - notifyListeners(); - } - }, - onError: (error) { - debugPrint('🚣 AWS Transcription Error: $error'); - }, - onDone: () { - debugPrint('🚣 AWS Transcription Stream Done'); - isAwsTranscribing = false; - notifyListeners(); - }, - ); - - debugPrint('🚣 AWS Transcription Started: ${response.sessionId}'); - } catch (e) { - debugPrint('🚣 Error starting AWS transcription: $e'); - isAwsTranscribing = false; - } - } - - String? previousSpeaker; - - // Helper method to process transcript events - String _processTranscriptEvent(TranscriptEvent event) { - if (event.transcript?.results == null || event.transcript!.results!.isEmpty) { - return currentAwsTranscript; // Return current to avoid clearing it - } - - final StringBuffer buffer = StringBuffer(); - bool hasNewCompletedSegments = false; - - for (final result in event.transcript!.results!) { - // Only process complete (non-partial) segments - if (result.isPartial == false && - result.alternatives != null && - result.alternatives!.isNotEmpty) { - - hasNewCompletedSegments = true; - final alternative = result.alternatives!.first; - - // Get speaker information if available - String? speaker; - if (alternative.items != null && alternative.items!.isNotEmpty) { - for (final item in alternative.items!) { - if (item.speaker != null && item.speaker!.isNotEmpty) { - final speakerId = int.tryParse(item.speaker!) ?? 0; - speaker = "Speaker ${speakerId + 1}"; - break; - } - } - } else if (result.channelId != null) { - speaker = "Channel ${result.channelId}"; - } - - // Add transcript with speaker label - if (speaker != null && alternative.transcript != null) { - // Add an extra newline if the speaker changed - if (previousSpeaker != null && previousSpeaker != speaker) { - buffer.write('\n\n$speaker: ${alternative.transcript}'); - } else { - buffer.write('\n$speaker: ${alternative.transcript}'); - } - previousSpeaker = speaker; - } else if (alternative.transcript != null) { - buffer.write('\n${alternative.transcript}'); - } - } - } - - // Only update the transcript if we have new completed segments - if (hasNewCompletedSegments) { - // If we already have content, append to it - if (currentAwsTranscript.isNotEmpty) { - return currentAwsTranscript + buffer.toString(); - } - return buffer.toString().trim(); - } - - return currentAwsTranscript; - } - - // Helper method to update the displayed text - void _updateDisplayText() { - final buffer = StringBuffer(); - - // Debug log - // debugPrint('Updating display text with ${recognizedSegments.length} segments'); - - for (final segment in recognizedSegments) { - // Debug log - // debugPrint('Segment ${segment.index}: "${segment.text}" (Speaker: ${segment.speakerId ?? "Unknown"})'); - - if (segment.text.isNotEmpty) { - if (buffer.isNotEmpty) { - buffer.write('\n'); - } - // Make sure we have a speaker ID display string - final prefix = (segment.speakerId != null && segment.speakerId!.isNotEmpty) - ? segment.speakerId! - : 'Speaker Unknown'; - buffer.write('$prefix: ${segment.text}'); - } - } - - // final displayText = buffer.toString(); - // debugPrint('Setting display text: $displayText'); - - controller.value = TextEditingValue( - text: buffer.toString(), - selection: TextSelection.collapsed(offset: buffer.length), - ); - - notifyListeners(); // Make sure UI gets updated - } - - // Add a new segment of recognized text - void _addRecognizedSegment(String text, double start) { - recognizedSegments.add(RecognizedSegment( - index: currentIndex, - text: text, - start: start, - end: start, // Will be updated when segment ends - )); - - debugPrint('Added new segment $currentIndex: "$text" (${start.toStringAsFixed(2)}s - ${start.toStringAsFixed(2)}s)'); - } - - // Update an existing segment with improved recognition - void _updateRecognizedSegment(int index, String newText, {String? speakerId, Float32List? embedding}) { - final segmentIndex = recognizedSegments.indexWhere((s) => s.index == index); - if (segmentIndex != -1) { - if (newText.trim().isNotEmpty) { - recognizedSegments[segmentIndex].text = newText; - } - recognizedSegments[segmentIndex].isProcessed = true; - - // Make sure speakerId is not null or empty - if (speakerId != null && speakerId.isNotEmpty) { - recognizedSegments[segmentIndex].speakerId = speakerId; - debugPrint('Updated segment $index with speaker: $speakerId'); - } - - if (embedding != null) { - recognizedSegments[segmentIndex].speakerEmbedding = embedding; - } - - // _updateDisplayText(); - } - } - - // Replace the processSegmentOffline method with this version - Future processSegmentOffline(AudioSegment segment) async { - debugPrint('Processing segment ${segment.index} offline (${segment.samples.length} samples)'); - - if (segment.samples.isEmpty) { - debugPrint('Empty samples for segment ${segment.index}, skipping'); - return; - } - - try { - if (speechIsolate == null) { - debugPrint('Speech isolate not initialized, failing silently'); - return; - } - - // Debug current speaker count - debugPrint('Sending segment with current speaker count: $currentSpeakerCount'); - - // Use the isolate to process this segment - await speechIsolate!.processSegment(ProcessSegmentMessage( - samples: segment.samples, - sampleRate: sampleRate, - segmentIndex: segment.index, - recognizerConfigs: { - 'currentSpeakerCount': currentSpeakerCount, - }, - )); - - // Processing will continue asynchronously, and results will be handled by the listener - - } catch (e) { - debugPrint('Error processing segment ${segment.index} offline: $e'); - } - } - - Future processPendingSegments() async { - if (pendingSegments.isEmpty || isProcessingOffline) { - debugPrint('No pending segments to process or already processing'); - return; - } - - debugPrint('Processing ${pendingSegments.length} pending segments'); - isProcessingOffline = true; - - try { - // Create a local copy to process to avoid modification during iteration - final segmentsToProcess = List.from(pendingSegments); - pendingSegments.clear(); - - for (final segment in segmentsToProcess) { - debugPrint('Processing pending segment ${segment.index}'); - await processSegmentOffline(segment); - // Update display after each segment is processed - _updateDisplayText(); - } - } catch (e) { - debugPrint('Error processing pending segments: $e'); - } finally { - isProcessingOffline = false; - debugPrint('Finished processing pending segments'); - } - } - - Future toggleRecording() async { - if (recordState == RecordState.stop) { - controller.value = TextEditingValue( - text: "Initializing Yappy!, just a moment..." - ); - await startRecording(); - } else { - await stopRecording(); - } - } - - Future _createRecordingFilePath() async { - // final dir = await getApplicationDocumentsDirectory(); - final timestamp = DateTime.now().millisecondsSinceEpoch; - // return '${dir.path}/recording_$timestamp.wav'; - return '/storage/emulated/0/Documents/recording_$timestamp.wav'; - } - - Future startRecording() async { - if (!isInitialized) { - await initialize(); - } - - try { - if (await audioRecorder.hasPermission()) { - // Reset for new recording - currentSpeakerCount = 0; - recognizedSegments.clear(); - recordingFilePath = await _createRecordingFilePath(); - - // Start AWS transcription in parallel - await startAwsTranscription(); - - const config = RecordConfig( - encoder: AudioEncoder.pcm16bits, - sampleRate: 16000, - numChannels: 1, - ); - - final recordStream = await audioRecorder.startStream(config); - allAudioSamples.clear(); - currentTimestamp = 0.0; - currentIndex = 0; - - // State tracking variables - bool isCurrentlySpeaking = false; - double speechStartTime = 0.0; - currentSegmentSamples.clear(); - - recordState = RecordState.record; - controller.value = TextEditingValue(text: "Listening..."); - notifyListeners(); - - recordStream.listen( - (data) { - // Send to AWS - if (isAwsTranscribing && awsAudioStreamSink != null) { - try { - awsAudioStreamSink!.add(Uint8List.fromList(data)); - } catch (e) { - debugPrint('🚣 Error sending audio to AWS: $e'); - } - } - - final samplesFloat32 = convertBytesToFloat32(Uint8List.fromList(data)); - - // Always add to complete recording and update timestamp - allAudioSamples.add(samplesFloat32); - currentTimestamp += samplesFloat32.length / sampleRate; - - // Update audio visualization - audioSamplesNotifier.value = samplesFloat32 - .map((e) => (e * 100000).toInt()) - .toList(); - - // Process audio through VAD - final windowSize = vad!.config.sileroVad.windowSize; - int offset = 0; - while (offset + windowSize <= samplesFloat32.length) { - final windowBuffer = Float32List.sublistView(samplesFloat32, offset, offset + windowSize); - vad!.acceptWaveform(windowBuffer); - offset += windowSize; - } - - // Check current VAD state - bool speechDetected = vad!.isDetected(); - - // TRANSITION: Silence → Speech (START COLLECTING) - if (!isCurrentlySpeaking && speechDetected) { - debugPrint('🎙️ Speech started at: $currentTimestamp'); - isCurrentlySpeaking = true; - speechStartTime = currentTimestamp; - currentSegmentSamples.clear(); // Start fresh collection - - // Reset the recognizer for a new segment - onlineRecognizer!.reset(onlineStream!); - } - - // DURING SPEECH: Collect and process audio - if (isCurrentlySpeaking) { - // Add samples to the current segment - currentSegmentSamples.add(samplesFloat32); - - // Process with online recognizer for real-time feedback - onlineStream!.acceptWaveform( - samples: samplesFloat32, - sampleRate: sampleRate - ); - - while (onlineRecognizer!.isReady(onlineStream!)) { - onlineRecognizer!.decode(onlineStream!); - } - - final text = onlineRecognizer!.getResult(onlineStream!).text; - - // Update display with current recognition - final existingSegmentIndex = recognizedSegments.indexWhere((s) => s.index == currentIndex); - - if (existingSegmentIndex != -1) { - // Update existing segment - debugPrint('Updated segment #$currentIndex of ${recognizedSegments.length}'); - recognizedSegments[existingSegmentIndex].text = text.isEmpty ? recognizedSegments[existingSegmentIndex].text : text; - } else { - // Add new segment - debugPrint('Adding new segment $currentIndex with text: "$text"'); - _addRecognizedSegment(text, speechStartTime); - } - - _updateDisplayText(); - } - - // TRANSITION: Speech → Silence (SAVE SEGMENT & STOP COLLECTING) - if (isCurrentlySpeaking && !speechDetected) { - debugPrint('🔇 Speech ended at: $currentTimestamp, duration: ${currentTimestamp - speechStartTime}'); - isCurrentlySpeaking = false; - - // Create and add a new audio segment for background processing - pendingSegments.add(AudioSegment( - samples: vad!.front().samples, - sampleRate: sampleRate, - index: currentIndex, - start: speechStartTime, - end: currentTimestamp, - )); - - // Process with offline recognizer in the background - processPendingSegments(); - - // Increment for next segment - currentIndex += 1; - - // Clear current segment samples - vad!.pop(); - - // During silence we don't collect samples - they're effectively discarded - // until the next speech segment begins - } - - // Process any complete segments from VAD - while (!vad!.isEmpty()) { - final segment = vad!.front(); - debugPrint('💬 VAD segment at: ${segment.start / sampleRate}s - ${currentTimestamp}s'); - vad!.pop(); - } - }, - onError: (error) { - debugPrint('Error from audio stream: $error'); - }, - onDone: () { - debugPrint('🫳🎤 Audio stream done; ${recognizedSegments.length} segments with $currentSpeakerCount speakers'); - // Clear VAD buffer - flush any pending segments - vad?.flush(); - awsAudioStreamSink?.close(); - }, - ); - } - } catch (e) { - debugPrint('Error starting recording: $e'); - } - } - - Future saveWavFile() async { - if (allAudioSamples.isEmpty || recordingFilePath == null) return; - - try { - // Combine all audio samples - final totalSamples = allAudioSamples.fold(0, (sum, list) => sum + list.length); - final combinedSamples = Float32List(totalSamples); - - var offset = 0; - for (var samples in allAudioSamples) { - combinedSamples.setRange(offset, offset + samples.length, samples); - offset += samples.length; - } - - // Convert Float32List to Int16List for WAV format - final int16Samples = Int16List(combinedSamples.length); - for (var i = 0; i < combinedSamples.length; i++) { - // Scale and clamp to Int16 range - final scaledSample = combinedSamples[i] * 32767; - int16Samples[i] = scaledSample.clamp(-32768, 32767).toInt(); - } - - // Create WAV file - final file = File(recordingFilePath!); - final sink = file.openWrite(); - - // WAV header (44 bytes) - final header = ByteData(44); - - // RIFF chunk descriptor - header.setUint8(0, 0x52); // 'R' - header.setUint8(1, 0x49); // 'I' - header.setUint8(2, 0x46); // 'F' - header.setUint8(3, 0x46); // 'F' - - // Chunk size (file size - 8) - final dataSize = int16Samples.length * 2; // 2 bytes per sample - final fileSize = 36 + dataSize; - header.setUint32(4, fileSize, Endian.little); - - // Format ('WAVE') - header.setUint8(8, 0x57); // 'W' - header.setUint8(9, 0x41); // 'A' - header.setUint8(10, 0x56); // 'V' - header.setUint8(11, 0x45); // 'E' - - // 'fmt ' subchunk - header.setUint8(12, 0x66); // 'f' - header.setUint8(13, 0x6D); // 'm' - header.setUint8(14, 0x74); // 't' - header.setUint8(15, 0x20); // ' ' - - // Subchunk1 size (16 for PCM) - header.setUint32(16, 16, Endian.little); - - // Audio format (1 for PCM) - header.setUint16(20, 1, Endian.little); - - // Number of channels (1 for mono) - header.setUint16(22, 1, Endian.little); - - // Sample rate - header.setUint32(24, sampleRate, Endian.little); - - // Byte rate (SampleRate * NumChannels * BitsPerSample/8) - header.setUint32(28, sampleRate * 1 * 16 ~/ 8, Endian.little); - - // Block align (NumChannels * BitsPerSample/8) - header.setUint16(32, 1 * 16 ~/ 8, Endian.little); - - // Bits per sample - header.setUint16(34, 16, Endian.little); - - // 'data' subchunk - header.setUint8(36, 0x64); // 'd' - header.setUint8(37, 0x61); // 'a' - header.setUint8(38, 0x74); // 't' - header.setUint8(39, 0x61); // 'a' - - // Subchunk2 size (data size) - header.setUint32(40, dataSize, Endian.little); - - // Write header - sink.add(header.buffer.asUint8List()); - - // Write data - final dataBytes = int16Samples.buffer.asUint8List(); - sink.add(dataBytes); - - await sink.close(); - - debugPrint('WAV file saved to: $recordingFilePath'); - } catch (e) { - debugPrint('Error saving WAV file: $e'); - } - } - - Future createConversation() async { - // Ensure WAV file is saved - debugPrint('Saving WAV file'); - await saveWavFile(); - - // Create conversation object - return Conversation( - segments: List.from(recognizedSegments), // Make a copy - audioFilePath: recordingFilePath ?? '', - awsTranscription: currentAwsTranscript, - ); - } - - // Properly stop AWS transcription - Future stopAwsTranscription() async { - if (isAwsTranscribing) { - try { - // Close the audio sink to indicate end of stream - await awsAudioStreamSink?.close(); - - // Give AWS a moment to process final audio - await Future.delayed(const Duration(milliseconds: 500)); - - // Then cancel the subscription - await awsTranscriptSubscription?.cancel(); - } catch (e) { - debugPrint('🚣 Error stopping AWS transcription: $e'); - } finally { - isAwsTranscribing = false; - awsAudioStreamSink = null; - awsTranscriptSubscription = null; - } - } - } - - Future stopRecording() async { - debugPrint('Stopping recording'); - - try { - // Update UI immediately to show we're stopping - recordState = RecordState.stop; - notifyListeners(); - - // Process any remaining audio - if (currentSegmentSamples.isNotEmpty) { - debugPrint('Processing final segment $currentIndex'); - - // Combine all Float32Lists into a single one - final combinedSamples = Float32List(currentSegmentSamples.fold( - 0, (sum, list) => sum + list.length)); - var offset = 0; - for (var samples in currentSegmentSamples) { - combinedSamples.setRange(offset, offset + samples.length, samples); - offset += samples.length; - } - - final segmentStart = recognizedSegments.lastOrNull?.start ?? 0.0; - - pendingSegments.add(AudioSegment( - samples: combinedSamples, - sampleRate: sampleRate, - index: currentIndex, - start: segmentStart, - end: currentTimestamp, - )); - } - - // Clean up resources - if (onlineStream != null) { - onlineStream!.free(); - onlineStream = onlineRecognizer?.createStream(); - } - - await audioRecorder.stop(); - - // Stop AWS transcription properly - await stopAwsTranscription(); - - // Process final segments - debugPrint('Processing final segments with offline recognizer'); - await processPendingSegments(); - - // Create conversation object (also saves the WAV file) - debugPrint('Creating conversation object'); - lastConversation = await createConversation(); - - currentSegmentSamples.clear(); - - _updateDisplayText(); - - debugPrint('Recording stopped successfully'); - } catch (e) { - debugPrint('Error stopping recording: $e'); - } finally { - notifyListeners(); - } - } - - @override - void dispose() { - controller.dispose(); - audioRecorder.dispose(); - onlineStream?.free(); - onlineRecognizer?.free(); - vad?.free(); - speechIsolate?.dispose(); - super.dispose(); - } - - // Add method to get AWS transcription - String getAwsRecordedText() { - if (lastConversation != null) { - return lastConversation!.getAwsTranscript(); - } else { - return "No AWS transcription available."; - } - } - - getRecordedText() { - if (lastConversation != null) { - return lastConversation!.getTranscript(); - } else { - return "No recording available."; - } - } -} diff --git a/team_b/yappy/lib/services/toast_service.dart b/team_b/yappy/lib/services/toast_service.dart deleted file mode 100644 index 489e52e7..00000000 --- a/team_b/yappy/lib/services/toast_service.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:async'; - -/// A service that manages toast notifications through stream controllers -class ToastService { - // Singleton pattern - static final ToastService _instance = ToastService._internal(); - factory ToastService() => _instance; - ToastService._internal(); - - // Stream controllers - final _messageController = StreamController.broadcast(); - final _progressController = StreamController.broadcast(); - final _visibilityController = StreamController.broadcast(); - final _successController = StreamController.broadcast(); - final _errorController = StreamController.broadcast(); - - // Track toast visibility - bool _isToastVisible = false; - - // Stream getters - Stream get messageStream => _messageController.stream; - Stream get progressStream => _progressController.stream; - Stream get visibilityStream => _visibilityController.stream; - Stream get successStream => _successController.stream; - Stream get errorStream => _errorController.stream; - - // Check if toast is visible (i.e., download in progress) - bool get isToastVisible => _isToastVisible; - - // Methods to control the toast - void showToast(String message, {double progress = 0.0}) { - _messageController.add(message); - _progressController.add(progress); - _visibilityController.add(true); - _isToastVisible = true; - } - - void updateProgress(double progress) { - _progressController.add(progress); - } - - void updateMessage(String message) { - _messageController.add(message); - } - - void hideToast() { - _visibilityController.add(false); - _isToastVisible = false; - } - - void showSuccess(String message) { - _successController.add(message); - } - - void showError(String message) { - _errorController.add(message); - } - - // Clean up resources - void dispose() { - _messageController.close(); - _progressController.close(); - _visibilityController.close(); - _successController.close(); - _errorController.close(); - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/services/transcription.dart b/team_b/yappy/lib/services/transcription.dart deleted file mode 100644 index 115e9545..00000000 --- a/team_b/yappy/lib/services/transcription.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:collection'; -import 'dart:convert'; - -import 'models.dart'; - -/// Converts a stream of [TranscriptEvent]s into a single [String]. -final class TranscriptEventStreamDecoder - extends Converter { - /// Creates a [TranscriptEventStreamDecoder] with a given - /// [TranscriptionBuildingStrategy]. - const TranscriptEventStreamDecoder(this.transcriptionBuildingStrategy); - - /// The strategy to use for building a transcription from a list of [Result]s. - final TranscriptionBuildingStrategy transcriptionBuildingStrategy; - - @override - String convert(TranscriptEvent input) { - return transcriptionBuildingStrategy - .buildTranscription(input.transcript?.results ?? []); - } - - @override - Sink startChunkedConversion(Sink sink) => - _TranscriptEventStreamConversionSink(sink, transcriptionBuildingStrategy); -} - -final class _TranscriptEventStreamConversionSink - implements ChunkedConversionSink { - _TranscriptEventStreamConversionSink( - this.sink, this.transcriptionBuildingStrategy); - - final Sink sink; - final TranscriptionBuildingStrategy transcriptionBuildingStrategy; - - final LinkedHashMap _results = - LinkedHashMap(); - - @override - void add(TranscriptEvent chunk) { - for (final Result result in chunk.transcript?.results ?? []) { - _results[result.resultId!] = result; - } - - sink.add(transcriptionBuildingStrategy.buildTranscription(_results.values)); - } - - @override - void close() { - _results.clear(); - - sink.close(); - } -} - -/// A strategy for building a transcription from a list of [Result]s. -abstract interface class TranscriptionBuildingStrategy { - /// Creates a [TranscriptionBuildingStrategy]. - const TranscriptionBuildingStrategy(); - - /// Builds a transcription from a list of [Result]s. - String buildTranscription(Iterable results); -} - -/// A transcription strategy that simply concatenates the transcripts into -/// a plain text. -final class PlainTextTranscriptionStrategy - implements TranscriptionBuildingStrategy { - /// Creates a [PlainTextTranscriptionStrategy]. - const PlainTextTranscriptionStrategy(); - - @override - String buildTranscription(Iterable results) { - final buffer = StringBuffer(); - - for (final result in results) { - if (result.alternatives == null) continue; - - for (final alternative in result.alternatives!) { - buffer.write(alternative.transcript); - buffer.write(' '); - } - } - - return buffer.toString().trimRight(); - } -} diff --git a/team_b/yappy/lib/services/utils.dart b/team_b/yappy/lib/services/utils.dart deleted file mode 100644 index 12a044c8..00000000 --- a/team_b/yappy/lib/services/utils.dart +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2024 Xiaomi Corporation -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:flutter/services.dart' show rootBundle; -import 'dart:typed_data'; -import "dart:io"; - -// Copy the asset file from src to dst -Future copyAssetFile(String src, [String? dst]) async { - final Directory directory = await getApplicationCacheDirectory(); - dst ??= basename(src); - final target = join(directory.path, dst); - bool exists = await File(target).exists(); - - final data = await rootBundle.load(src); - - if (!exists || File(target).lengthSync() != data.lengthInBytes) { - final List bytes = - data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); - await File(target).writeAsBytes(bytes); - } - - return target; -} - -Float32List convertBytesToFloat32(Uint8List bytes, [endian = Endian.little]) { - final values = Float32List(bytes.length ~/ 2); - - final data = ByteData.view(bytes.buffer); - - for (var i = 0; i < bytes.length; i += 2) { - int short = data.getInt16(i, endian); - values[i ~/ 2] = short / 32678.0; - } - - return values; -} diff --git a/team_b/yappy/lib/services/vad_model.dart b/team_b/yappy/lib/services/vad_model.dart deleted file mode 100644 index 768c0888..00000000 --- a/team_b/yappy/lib/services/vad_model.dart +++ /dev/null @@ -1,12 +0,0 @@ -import './model_manager.dart'; - -Future getVadModel({required int type}) async { - final modelManager = ModelManager(); - - switch (type) { - case 0: - return await modelManager.getModelPath('vad', 'vad_model.onnx'); - default: - throw ArgumentError('Unsupported type: $type'); - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/settings_page.dart b/team_b/yappy/lib/settings_page.dart deleted file mode 100644 index 64ec7fbc..00000000 --- a/team_b/yappy/lib/settings_page.dart +++ /dev/null @@ -1,404 +0,0 @@ -import 'package:dart_openai/dart_openai.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import './services/model_manager.dart'; -import './main.dart'; -import 'package:yappy/theme_provider.dart'; - -class SettingsPage extends StatefulWidget { - const SettingsPage({super.key}); - - @override - State createState() => _SettingsPageState(); -} - -class _SettingsPageState extends State { - bool _modelsDownloaded = false; - bool _wifiOnlyDownloads = true; - bool _isLoading = true; - final ModelManager _modelManager = ModelManager(); - - @override - void initState() { - super.initState(); - _loadSettings(); - } - - Future _loadSettings() async { - try { - // Check models status - final exist = await _modelManager.modelsExist(); - - // Load Wi-Fi only setting - final preferences = await SharedPreferences.getInstance(); - final wifiOnly = preferences.getBool('wifi_only_downloads') ?? true; - - if (mounted) { - setState(() { - _modelsDownloaded = exist; - _wifiOnlyDownloads = wifiOnly; - _isLoading = false; - }); - } - } catch (e) { - if (mounted) { - setState(() { - _isLoading = false; - }); - } - } - } - - @override - Widget build(BuildContext context) { - final themeProvider = Provider.of(context); - - return Scaffold( - appBar: AppBar( - title: const Text('Settings'), - ), - body: _isLoading - ? const Center(child: CircularProgressIndicator()) - : ListView( - children: [ - // Original settings items - ListTile( - leading: const Icon(Icons.settings), - title: const Text('About Yappy'), - onTap: () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Version'), - content: const Text('Version 1.0.0'), - actions: [ - TextButton( - child: const Text('OK'), - onPressed: () { - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - ], - ); - }, - ); - }, - ), - - SwitchListTile( - title: const Text('Dark Mode'), - subtitle: const Text('Toggle Dark Mode on or off'), - value: themeProvider.isDarkMode, - onChanged: (value) { - themeProvider.toggleTheme(value); - }, - ), - - const Divider(), - - ListTile( - leading: const Icon(Icons.key), - title: const Text('OpenAI API Key'), - subtitle: Text(preferences.getString('openai_api_key') != null - ? 'API Key set' - : 'Not configured'), - onTap: () => _showApiKeyDialog(), - ), - - ListTile( - leading: const Icon(Icons.cloud), - title: const Text('AWS Credentials'), - subtitle: Text(preferences.getBool('is_aws_configured')! - ? 'Credentials configured' - : 'Not configured'), - onTap: () => _showAwsCredentialsDialog(), - ), - - const Divider(), - - // Model management settings - const Padding( - padding: EdgeInsets.fromLTRB(16, 16, 16, 8), - child: Text( - 'AI Speech Models', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.grey, - ), - ), - ), - - ListTile( - leading: const Icon(Icons.cloud_download), - title: const Text('Speech Recognition Models'), - subtitle: Text(_modelsDownloaded - ? 'Downloaded and ready to use' - : 'Not downloaded'), - trailing: _modelsDownloaded - ? const Icon(Icons.check_circle, color: Colors.green) - : ElevatedButton( - onPressed: () => _handleModelDownload(context), - child: const Text('Download'), - ), - ), - - SwitchListTile( - secondary: const Icon(Icons.wifi), - title: const Text('Download on Wi-Fi only'), - subtitle: const Text( - 'When enabled, models will only download when connected to Wi-Fi'), - value: _wifiOnlyDownloads, - onChanged: (value) => _updateWifiOnlySetting(value), - ), - - if (_modelsDownloaded) - ListTile( - leading: const Icon(Icons.delete_outline), - title: const Text('Delete Models'), - subtitle: const Text('Free up space by removing downloaded models'), - onTap: () => _handleDeleteModels(context), - ), - ], - ), - ); - } - - // Show dialog to set OpenAI API key - void _showApiKeyDialog() { - final TextEditingController apiKeyController = TextEditingController(); - - // Pre-fill with existing key if available - final existingKey = preferences.getString('openai_api_key'); - if (existingKey != null) { - apiKeyController.text = existingKey; - } - - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Enter OpenAI API Key'), - content: TextField( - controller: apiKeyController, - decoration: const InputDecoration(hintText: "API Key"), - ), - actions: [ - TextButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: const Text('Save'), - onPressed: () async { - // Save the API key using SettingsManager - String apiKey = apiKeyController.text; - await preferences.setString('openai_api_key', apiKey); - OpenAI.apiKey = apiKey; // Also set it for current session - - if (!context.mounted) return; - Navigator.of(context).pop(); - - // Show confirmation to user - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('API Key saved successfully'), - duration: Duration(seconds: 2), - ), - ); - }, - ), - ], - ); - }, - ); - } - - // Show dialog to set AWS credentials - void _showAwsCredentialsDialog() { - final TextEditingController accessKeyController = TextEditingController(); - final TextEditingController secretKeyController = TextEditingController(); - final TextEditingController regionController = TextEditingController(); - - // Pre-fill with existing values if available - final existingAccessKey = preferences.getString('aws_access_key'); - final existingSecretKey = preferences.getString('aws_secret_key'); - final existingRegion = preferences.getString('aws_region'); - - if (existingAccessKey != null) { - accessKeyController.text = existingAccessKey; - } - if (existingSecretKey != null) { - secretKeyController.text = existingSecretKey; - } - if (existingRegion != null) { - regionController.text = existingRegion; - } - - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Enter AWS Credentials'), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: accessKeyController, - decoration: const InputDecoration( - hintText: "AWS Access Key", - labelText: "Access Key", - ), - ), - const SizedBox(height: 10), - TextField( - controller: secretKeyController, - decoration: const InputDecoration( - hintText: "AWS Secret Key", - labelText: "Secret Key", - ), - obscureText: true, // Hide sensitive information - ), - const SizedBox(height: 10), - TextField( - controller: regionController, - decoration: const InputDecoration( - hintText: "AWS Region (e.g., us-east-1)", - labelText: "Region", - ), - ), - ], - ), - ), - actions: [ - TextButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - TextButton( - child: const Text('Save'), - onPressed: () async { - // Save AWS credentials using SettingsManager - String accessKey = accessKeyController.text; - String secretKey = secretKeyController.text; - String region = regionController.text; - - await preferences.setString('aws_access_key', accessKey); - await preferences.setString('aws_secret_key', secretKey); - await preferences.setString('aws_region', region); - await preferences.setBool('is_aws_configured', true); - - if (!context.mounted) return; - Navigator.of(context).pop(); - - // Show confirmation to user - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('AWS credentials saved successfully'), - duration: Duration(seconds: 2), - ), - ); - }, - ), - ], - ); - }, - ); - } - - // Update WiFi only setting - Future _updateWifiOnlySetting(bool value) async { - setState(() { - _wifiOnlyDownloads = value; - }); - - await preferences.setBool('wifi_only_downloads', value); - await _modelManager.saveWifiOnlySetting(value); - } - - Future _handleModelDownload(BuildContext context) async { - await _modelManager.downloadModelsFromSettings(context); - - if (!mounted) return; - - // Refresh the model status - final exist = await _modelManager.modelsExist(); - - if (!mounted) return; - - setState(() { - _modelsDownloaded = exist; - }); - - await preferences.setBool('models_downloaded', exist); - } - - Future _handleDeleteModels(BuildContext context) async { - final shouldDelete = await showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - title: const Text('Delete Models?'), - content: const Text( - 'This will delete all downloaded speech models. ' - 'You will need to download them again to use the app\'s ' - 'speech recognition features.' - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(dialogContext).pop(false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(dialogContext).pop(true), - child: const Text('Delete'), - ), - ], - ), - ) ?? false; - - if (!shouldDelete || !mounted) return; - - final success = await _modelManager.deleteModels(); - - if (!mounted) return; - - setState(() { - _modelsDownloaded = !success; - }); - - // Update models status in settings - await preferences.setBool('models_downloaded', !success); - - _showResultSnackBar(success); - } - - // Show result snackbar - void _showResultSnackBar(bool success) { - if (success) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Models deleted successfully'), - duration: Duration(seconds: 2), - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Failed to delete models'), - backgroundColor: Colors.red, - duration: Duration(seconds: 2), - ), - ); - } - } - -} \ No newline at end of file diff --git a/team_b/yappy/lib/sign_up_page.dart b/team_b/yappy/lib/sign_up_page.dart deleted file mode 100644 index 90e6369a..00000000 --- a/team_b/yappy/lib/sign_up_page.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter/material.dart'; - -class SignUpPage extends StatefulWidget { - const SignUpPage({super.key}); - - @override - SignUpPageState createState() => SignUpPageState(); -} - -class SignUpPageState extends State { - final TextEditingController _usernameController = TextEditingController(); - final TextEditingController _passwordController = TextEditingController(); - final TextEditingController _reenterPasswordController = TextEditingController(); - - @override - void dispose() { - // Dispose controllers to prevent memory leaks - _usernameController.dispose(); - _passwordController.dispose(); - _reenterPasswordController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.black, - body: Center( - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CircleAvatar( - backgroundColor: Colors.white, - radius: 30, - child: Icon(Icons.chat, color: Colors.green, size: 40), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 80), - ), - child: const Text( - 'Sign-Up', - style: TextStyle(color: Colors.white, fontSize: 18), - ), - ), - const SizedBox(height: 20), - TextField( - controller: _usernameController, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - filled: true, - fillColor: Colors.grey[900], - border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), - labelText: 'Username', - labelStyle: const TextStyle(color: Colors.white), - ), - ), - const SizedBox(height: 10), - TextField( - controller: _passwordController, - obscureText: true, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - filled: true, - fillColor: Colors.grey[900], - border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), - labelText: 'Password', - labelStyle: const TextStyle(color: Colors.white), - ), - ), - const SizedBox(height: 10), - TextField( - controller: _reenterPasswordController, - obscureText: true, - style: const TextStyle(color: Colors.white), - decoration: InputDecoration( - filled: true, - fillColor: Colors.grey[900], - border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), - labelText: 'Re-Enter Password', - labelStyle: const TextStyle(color: Colors.white), - ), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () {}, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 40), - ), - child: const Text( - 'Submit', - style: TextStyle(color: Colors.white, fontSize: 16), - ), - ), - const SizedBox(height: 30), - const Text( - 'Yappy! Is not responsible for any legal consequences due to the use of this application', - textAlign: TextAlign.center, - style: TextStyle(color: Colors.white60, fontSize: 12), - ), - ], - ), - ), - ), - ); - } -} diff --git a/team_b/yappy/lib/theme_provider.dart b/team_b/yappy/lib/theme_provider.dart deleted file mode 100644 index d6f1e0d0..00000000 --- a/team_b/yappy/lib/theme_provider.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class ThemeProvider with ChangeNotifier { - bool _isDarkMode = false; - - bool get isDarkMode => _isDarkMode; - - ThemeProvider() { - _loadThemePreference(); - } - - Future _loadThemePreference() async { - final prefs = await SharedPreferences.getInstance(); - _isDarkMode = prefs.getBool('toggle_setting') ?? false; - notifyListeners(); - } - - Future toggleTheme(bool value) async { - _isDarkMode = value; - final prefs = await SharedPreferences.getInstance(); - await prefs.setBool('toggle_setting', value); - notifyListeners(); - } - - ThemeData get themeData { - return _isDarkMode ? darkTheme : lightTheme; - } - - static final ThemeData lightTheme = ThemeData( - brightness: Brightness.light, - primarySwatch: Colors.lightGreen, - visualDensity: VisualDensity.adaptivePlatformDensity, - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.black, - ), - ), - scaffoldBackgroundColor: Colors.white, - iconTheme: const IconThemeData( - color: Colors.black, - ), - ); - - static final ThemeData darkTheme = ThemeData( - brightness: Brightness.dark, - primarySwatch: Colors.grey, - visualDensity: VisualDensity.adaptivePlatformDensity, - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: Color.fromARGB(255, 79, 79, 83), - foregroundColor: Colors.white, - ), - ), - scaffoldBackgroundColor: Colors.black, - iconTheme: const IconThemeData( - color: Colors.white, - ), - ); -} diff --git a/team_b/yappy/lib/toast_widget.dart b/team_b/yappy/lib/toast_widget.dart deleted file mode 100644 index 574b07ae..00000000 --- a/team_b/yappy/lib/toast_widget.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:flutter/material.dart'; -import 'services/toast_service.dart'; - -/// Widget that displays persistent toast notifications across all screens -class ToastWidget extends StatefulWidget { - final Widget child; - - const ToastWidget({super.key, required this.child}); - - @override - State createState() => _ToastWidgetState(); -} - -class _ToastWidgetState extends State { - final _toastService = ToastService(); - bool _visible = false; - String _message = ''; - double _progress = 0.0; - - @override - void initState() { - super.initState(); - - // Listen to service streams - _toastService.messageStream.listen((message) { - if (mounted) { - setState(() => _message = message); - } - }); - - _toastService.progressStream.listen((progress) { - if (mounted) { - setState(() => _progress = progress); - } - }); - - _toastService.visibilityStream.listen((visible) { - if (mounted) { - setState(() => _visible = visible); - } - }); - - _toastService.successStream.listen(_showSuccessSnackBar); - _toastService.errorStream.listen(_showErrorSnackBar); - } - - void _showSuccessSnackBar(String message) { - // Use ScaffoldMessenger to show SnackBar globally - final messenger = ScaffoldMessenger.of(context); - - messenger.clearSnackBars(); - messenger.showSnackBar( - SnackBar( - content: Text(message), - backgroundColor: Colors.green, - duration: const Duration(seconds: 3), - behavior: SnackBarBehavior.floating, - ), - ); - } - - void _showErrorSnackBar(String message) { - // Use ScaffoldMessenger to show SnackBar globally - final messenger = ScaffoldMessenger.of(context); - - messenger.clearSnackBars(); - messenger.showSnackBar( - SnackBar( - content: Text(message), - backgroundColor: Colors.red, - duration: const Duration(seconds: 5), - behavior: SnackBarBehavior.floating, - ), - ); - } - - @override - Widget build(BuildContext context) { - return Material( - // Ensure Material is available for elevation and visual elements - type: MaterialType.transparency, - child: Stack( - children: [ - // Main content - widget.child, - - // Toast notification - AnimatedPositioned( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - bottom: _visible ? 32.0 : -100.0, - left: 16.0, - right: 16.0, - child: SafeArea( - child: Material( - elevation: 4.0, - borderRadius: BorderRadius.circular(8.0), - color: Colors.black87, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 12.0, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator( - strokeWidth: 2.0, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ), - const SizedBox(width: 12.0), - Expanded( - child: Text( - _message, - style: const TextStyle( - color: Colors.white, - fontSize: 14.0, - ), - ), - ), - ], - ), - const SizedBox(height: 8.0), - LinearProgressIndicator( - value: _progress, - backgroundColor: Colors.white24, - valueColor: const AlwaysStoppedAnimation(Colors.white), - ), - ], - ), - ), - ), - ), - ), - ], - ), - ); - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/tool_bar.dart b/team_b/yappy/lib/tool_bar.dart deleted file mode 100644 index 26f25568..00000000 --- a/team_b/yappy/lib/tool_bar.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:yappy/home_page.dart'; -import 'package:yappy/restaurant.dart'; -import 'package:yappy/contact_page.dart'; -import 'package:yappy/help.dart'; -import 'package:yappy/medical_patient.dart'; -import 'package:yappy/medical_doctor.dart'; -import 'package:yappy/mechanic.dart'; -import 'package:yappy/settings_page.dart'; -import 'package:yappy/theme_provider.dart'; - -// Defines a reusable Hamburger Menu Widget (AppBar + Drawer) -class ToolBar extends StatelessWidget { - final bool showHamburger; - - const ToolBar({super.key, this.showHamburger = true}); - - @override - Widget build(BuildContext context) { - double screenWidth = MediaQuery.of(context).size.width; - double screenHeight = MediaQuery.of(context).size.height; - final themeProvider = Provider.of(context); - - return AppBar( - // Background color based on the theme - backgroundColor: themeProvider.isDarkMode ? Colors.black : Colors.white, - leading: showHamburger - ? Builder( - builder: (context) { - return IconButton( - icon: Icon(Icons.menu, color: themeProvider.isDarkMode ? Colors.white : Colors.black), - onPressed: () { - Scaffold.of(context).openDrawer(); - }, - ); - }, - ) : SizedBox(width: screenHeight * 0.08), - toolbarHeight: screenHeight * 0.11, - title: Center( - child: CircleAvatar( - backgroundColor: themeProvider.isDarkMode ? Colors.black : Colors.white, - radius: screenWidth * 0.2, - child: Image.asset( - 'assets/icon/app_icon.png', - width: screenWidth * 0.22, - height: screenWidth * 0.22, - ), - ), - ), - actions: [ - // Information button with dynamic color - IconButton( - icon: Icon(Icons.info, color: themeProvider.isDarkMode ? Colors.white : Colors.green), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Information', style: TextStyle(color: themeProvider.isDarkMode ? Colors.white : Colors.black)), - content: Text( - 'Yappy Terms & Conditions\n\nBy using Yappy, you agree to use the app responsibly and comply with all applicable laws. Respect user privacy and refrain from harmful or abusive behavior.\n\nUnderstand that Yappy is not liable for any misuse or legal consequences arising from its use. We may update these terms as needed.\n\nContinued use of Yappy means acceptance of any changes.', - style: TextStyle(color: themeProvider.isDarkMode ? Colors.white : Colors.black), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('OK', style: TextStyle(color: themeProvider.isDarkMode ? Colors.white : Colors.black)), - ), - ], - ); - }, - ); - }, - ), - ], - ); - } -} - -// Creates the hamburger menu -class HamburgerDrawer extends StatelessWidget { - const HamburgerDrawer({super.key}); - - @override - Widget build(BuildContext context) { - final themeProvider = Provider.of(context); - - double screenWidth = MediaQuery.of(context).size.width; - return SafeArea( - child: Drawer( - width: screenWidth * .45, - // Background color based on theme - backgroundColor: themeProvider.isDarkMode ? Color.fromARGB(255, 79, 79, 83) : Colors.green, - child: ListView( - padding: EdgeInsets.zero, - children: [ - _buildDrawerItem('Home', context, HomePage(), themeProvider), - _buildDrawerItem('Restaurant', context, RestaurantPage(), themeProvider), - _buildDrawerItem('Vehicle Maintenance', context, MechanicalAidPage(), themeProvider), - _buildDrawerItem('Medical Doctor', context, MedicalDoctorPage(), themeProvider), - _buildDrawerItem('Medical Patient', context, MedicalPatientPage(), themeProvider), - _buildDrawerItem('Help', context, HelpPage(), themeProvider), - _buildDrawerItem('Contact', context, ContactPage(), themeProvider), - _buildDrawerItem('Settings', context, SettingsPage(), themeProvider), - ], - ), - ), - ); - } - - // Creates the individual drawer items for the hamburger menu - Widget _buildDrawerItem(String title, BuildContext context, Widget page, ThemeProvider themeProvider) { - return ListTile( - contentPadding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), - textColor: themeProvider.isDarkMode ? Colors.white : Colors.black, // Text color based on theme - tileColor: themeProvider.isDarkMode ? Color.fromARGB(255, 54, 54, 54) : Colors.green.shade200, - title: Text( - title, - style: TextStyle(color: themeProvider.isDarkMode ? Colors.white : Colors.black), // Text color based on theme - ), - onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => page), - ); - }, - ); - } -} diff --git a/team_b/yappy/lib/transcript_dialog.dart b/team_b/yappy/lib/transcript_dialog.dart deleted file mode 100644 index 70b30054..00000000 --- a/team_b/yappy/lib/transcript_dialog.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'package:flutter/material.dart'; - -class TranscriptDialog extends StatefulWidget { - final String localTranscript; - final String awsTranscript; - final bool awsAvailable; - final int userId; - final int transcriptId; - final String industry; - final Function(int userId, int transcriptId, String text, String industry) onSave; - - const TranscriptDialog({ - super.key, - required this.localTranscript, - required this.awsTranscript, - required this.awsAvailable, - required this.userId, - required this.transcriptId, - required this.industry, - required this.onSave, - }); - - @override - State createState() => _TranscriptDialogState(); -} - -class _TranscriptDialogState extends State { - late TextEditingController _localController; - late TextEditingController _awsController; - bool _showAwsTranscript = false; - - @override - void initState() { - super.initState(); - _localController = TextEditingController(text: widget.localTranscript); - _awsController = TextEditingController(text: widget.awsTranscript); - if (!widget.awsAvailable) { - _showAwsTranscript = false; - } - } - - @override - void dispose() { - _localController.dispose(); - _awsController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Row( - children: [ - Text('Edit Transcript'), - Spacer(), - ToggleButtons( - isSelected: [!_showAwsTranscript, _showAwsTranscript], - onPressed: (index) { - if (index == 0 || widget.awsAvailable) { - setState(() { - _showAwsTranscript = index == 1; - }); - } else if (index == 1 && !widget.awsAvailable) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('AWS transcription is not available'), - duration: Duration(seconds: 2), - behavior: SnackBarBehavior.fixed, - ), - ); - } - }, - borderRadius: BorderRadius.circular(8), - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text('Local'), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text( - 'AWS', - style: TextStyle( - color: widget.awsAvailable - ? null - : Theme.of(context).disabledColor, - ), - ), - ), - ], - ), - ], - ), - content: SizedBox( - width: double.maxFinite, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (_showAwsTranscript) - Text( - 'AWS Transcription', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.orange, - ), - ) - else - Text( - 'Local Transcription', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.green, - ), - ), - SizedBox(height: 8), - TextField( - controller: _showAwsTranscript ? _awsController : _localController, - decoration: InputDecoration( - hintText: 'Edit the transcript text', - border: OutlineInputBorder(), - ), - maxLines: 10, - ), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () async { - // Save the currently selected transcript - final textToSave = _showAwsTranscript - ? _awsController.text - : _localController.text; - - await widget.onSave( - widget.userId, - widget.transcriptId, - textToSave, - widget.industry, - ); - - if (context.mounted) { - Navigator.of(context).pop(); - } - }, - child: Text('Save'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text('Cancel'), - ), - ], - ); - } -} \ No newline at end of file diff --git a/team_b/yappy/lib/transcription_box.dart b/team_b/yappy/lib/transcription_box.dart deleted file mode 100644 index e2a59642..00000000 --- a/team_b/yappy/lib/transcription_box.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:yappy/theme_provider.dart'; - -class TranscriptionBox extends StatefulWidget { - final TextEditingController controller; - - const TranscriptionBox({ - required this.controller, - super.key, - }); - - @override - TranscriptionBoxState createState() => TranscriptionBoxState(); -} - -class TranscriptionBoxState extends State { - final ScrollController _scrollController = ScrollController(); - - @override - void initState() { - super.initState(); - widget.controller.addListener(_scrollToBottom); - } - - @override - void dispose() { - widget.controller.removeListener(_scrollToBottom); - _scrollController.dispose(); - super.dispose(); - } - - void _scrollToBottom() { - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent, - duration: Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - }); - } - - @override - Widget build(BuildContext context) { - final themeProvider = Provider.of(context); - double screenHeight = MediaQuery.of(context).size.height; - - return SizedBox( - height: screenHeight * 0.4, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.0), - color: themeProvider.isDarkMode ? const Color.fromARGB(255, 67, 67, 67): Colors.green, - ), - child: Scrollbar( - controller: _scrollController, - child: SingleChildScrollView( - controller: _scrollController, - scrollDirection: Axis.vertical, - child: TextField( - controller: widget.controller, - maxLines: null, - readOnly: true, - decoration: InputDecoration( - hintText: "", - hintStyle: TextStyle(color:Colors.white), - border: InputBorder.none, - contentPadding: EdgeInsets.all(10), - ), - style: TextStyle( - color: Colors.white, - fontSize: 18, - ), - ), - ), - ), - ), - ); - } -} - diff --git a/team_b/yappy/lib/tutorial_page.dart b/team_b/yappy/lib/tutorial_page.dart deleted file mode 100644 index d1327581..00000000 --- a/team_b/yappy/lib/tutorial_page.dart +++ /dev/null @@ -1,169 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:yappy/audiowave_widget.dart'; -import 'package:yappy/home_page.dart'; -import 'package:yappy/services/speech_state.dart'; -import 'package:yappy/tool_bar.dart'; -import 'package:yappy/industry_menu.dart'; -import 'package:yappy/transcription_box.dart'; -import 'services/model_manager.dart'; - - -class MedicalPatientApp extends StatelessWidget { - const MedicalPatientApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: TutorialPage(), - ); - } -} -//Creates a page for the Medical Patient industry -//The page will contain the industry menu and the transcription box -class TutorialPage extends StatelessWidget { - TutorialPage({super.key}); - final speechState = SpeechState(); - final modelManager = ModelManager(); - - @override - Widget build(BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _showTutorialPopup(context); - }); - - return Scaffold( - backgroundColor: const Color.fromARGB(255, 0, 0, 0), - appBar: PreferredSize( - preferredSize: Size.fromHeight(140), - child: ToolBar() - ), - drawer: HamburgerDrawer(), - body: ListenableBuilder( - listenable: speechState, - builder: (context, child) { - return Column( - children: [ - IndustryMenu( - title: "Tutorial", - icon: Icons.local_pharmacy, - speechState: speechState, - modelManager: modelManager, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: SingleChildScrollView( - child: Column( - children: [ - AudiowaveWidget(speechState: speechState), - TranscriptionBox( - controller: speechState.controller, - ), - ], - ), - ) - - ), - ), - ], - ); - } - ), - ); - } - - void _showTutorialPopup(BuildContext context) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("The button of the left is the Record button that allows you to record conversations and get a transcript in return."), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - _showSecondPopup(context); - }, - child: Text("Next"), - ), - ], - ), - ); - }, - ); - } - - void _showSecondPopup(BuildContext context) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("The second button will show you the days transcripts with broken down into details."), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - _showThirdPopup(context); - }, - child: Text("Next"), - ), - ], - ), - ); - }, - ); - } - - void _showThirdPopup(BuildContext context) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("The third button will show you all transcripts and allow you to search, share, upload, download, and delete."), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - _showFourthPopup(context); - }, - child: Text("Next"), - ), - ], - ), - ); - }, - ); - } - - void _showFourthPopup(BuildContext context) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("The fourth button will bring you to a chatbot that can search your transcripts to provide you information."), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - Navigator.of(context).pushReplacement( - - MaterialPageRoute(builder: (context) => HomePage()) - ); - }, - child: Text("Finish"), - ), - ], - ), - ); - }, - ); - } -} diff --git a/team_b/yappy/linux/.gitignore b/team_b/yappy/linux/.gitignore deleted file mode 100644 index 24ce4374..00000000 --- a/team_b/yappy/linux/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -flutter/ephemeral -flutter/generated* -**/flutter/generated* \ No newline at end of file diff --git a/team_b/yappy/linux/CMakeLists.txt b/team_b/yappy/linux/CMakeLists.txt deleted file mode 100644 index 9cddf947..00000000 --- a/team_b/yappy/linux/CMakeLists.txt +++ /dev/null @@ -1,128 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "yappy") -# The unique GTK application identifier for this application. See: -# https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.yappy") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(SET CMP0063 NEW) - -# Load bundled libraries from the lib/ directory relative to the binary. -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Root filesystem for cross-building. -if(FLUTTER_TARGET_PLATFORM_SYSROOT) - set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) - set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -endif() - -# Define build configuration options. -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") -endif() - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_14) - target_compile_options(${TARGET} PRIVATE -Wall -Werror) - target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") - target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) - -# Only the install-generated bundle's copy of the executable will launch -# correctly, since the resources must in the right relative locations. To avoid -# people trying to run the unbundled copy, put it in a subdirectory instead of -# the default top-level location. -set_target_properties(${BINARY_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" -) - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# By default, "installing" just makes a relocatable bundle in the build -# directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) - install(FILES "${bundled_library}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endforeach(bundled_library) - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") - install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() diff --git a/team_b/yappy/linux/flutter/CMakeLists.txt b/team_b/yappy/linux/flutter/CMakeLists.txt deleted file mode 100644 index d5bd0164..00000000 --- a/team_b/yappy/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) diff --git a/team_b/yappy/linux/runner/CMakeLists.txt b/team_b/yappy/linux/runner/CMakeLists.txt deleted file mode 100644 index e97dabc7..00000000 --- a/team_b/yappy/linux/runner/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the application ID. -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Add dependency libraries. Add any application-specific dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) - -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/team_b/yappy/linux/runner/main.cc b/team_b/yappy/linux/runner/main.cc deleted file mode 100644 index e7c5c543..00000000 --- a/team_b/yappy/linux/runner/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/team_b/yappy/linux/runner/my_application.cc b/team_b/yappy/linux/runner/my_application.cc deleted file mode 100644 index ee211132..00000000 --- a/team_b/yappy/linux/runner/my_application.cc +++ /dev/null @@ -1,130 +0,0 @@ -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication { - GtkApplication parent_instance; - char** dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "yappy"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "yappy"); - } - - gtk_window_set_default_size(window, 1280, 720); - gtk_widget_show(GTK_WIDGET(window)); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); - - FlView* view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { - MyApplication* self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GApplication::startup. -static void my_application_startup(GApplication* application) { - //MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application startup. - - G_APPLICATION_CLASS(my_application_parent_class)->startup(application); -} - -// Implements GApplication::shutdown. -static void my_application_shutdown(GApplication* application) { - //MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application shutdown. - - G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; - G_APPLICATION_CLASS(klass)->startup = my_application_startup; - G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -static void my_application_init(MyApplication* self) {} - -MyApplication* my_application_new() { - // Set the program name to the application ID, which helps various systems - // like GTK and desktop environments map this running application to its - // corresponding .desktop file. This ensures better integration by allowing - // the application to be recognized beyond its binary name. - g_set_prgname(APPLICATION_ID); - - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, - nullptr)); -} diff --git a/team_b/yappy/linux/runner/my_application.h b/team_b/yappy/linux/runner/my_application.h deleted file mode 100644 index 72271d5e..00000000 --- a/team_b/yappy/linux/runner/my_application.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/team_b/yappy/macos/.gitignore b/team_b/yappy/macos/.gitignore deleted file mode 100644 index 55610ecf..00000000 --- a/team_b/yappy/macos/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ -**/Flutter/GeneratedPluginRegistrant.* -Flutter/Generated* - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/team_b/yappy/macos/Flutter/Flutter-Debug.xcconfig b/team_b/yappy/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index c2efd0b6..00000000 --- a/team_b/yappy/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/team_b/yappy/macos/Flutter/Flutter-Release.xcconfig b/team_b/yappy/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index c2efd0b6..00000000 --- a/team_b/yappy/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/team_b/yappy/macos/Runner.xcodeproj/project.pbxproj b/team_b/yappy/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 2856f6d7..00000000 --- a/team_b/yappy/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,705 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC10EC2044A3C60003C045; - remoteInfo = Runner; - }; - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* yappy.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "yappy.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80D6294CF71000263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80D7294CF71000263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 331C80D6294CF71000263BE5 /* RunnerTests */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* yappy.app */, - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C80D1294CF70F00263BE5 /* Sources */, - 331C80D2294CF70F00263BE5 /* Frameworks */, - 331C80D3294CF70F00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80DA294CF71000263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* yappy.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80D4294CF70F00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 33CC10EC2044A3C60003C045; - }; - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 331C80D4294CF70F00263BE5 /* RunnerTests */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80D3294CF70F00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80D1294CF70F00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC10EC2044A3C60003C045 /* Runner */; - targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; - }; - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 331C80DB294CF71000263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.yappy.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/yappy.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/yappy"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.yappy.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/yappy.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/yappy"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.yappy.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/yappy.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/yappy"; - }; - name = Profile; - }; - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80DB294CF71000263BE5 /* Debug */, - 331C80DC294CF71000263BE5 /* Release */, - 331C80DD294CF71000263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/team_b/yappy/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/team_b/yappy/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/team_b/yappy/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/team_b/yappy/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/team_b/yappy/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3fd18668..00000000 --- a/team_b/yappy/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/team_b/yappy/macos/Runner.xcworkspace/contents.xcworkspacedata b/team_b/yappy/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16..00000000 --- a/team_b/yappy/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/team_b/yappy/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/team_b/yappy/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/team_b/yappy/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/team_b/yappy/macos/Runner/AppDelegate.swift b/team_b/yappy/macos/Runner/AppDelegate.swift deleted file mode 100644 index b3c17614..00000000 --- a/team_b/yappy/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Cocoa -import FlutterMacOS - -@main -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } - - override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { - return true - } -} diff --git a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f1..00000000 --- a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d9..00000000 Binary files a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eba..00000000 Binary files a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa4..00000000 Binary files a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bdb57226..00000000 Binary files a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index f083318e..00000000 Binary files a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index 326c0e72..00000000 Binary files a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632cf..00000000 Binary files a/team_b/yappy/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/team_b/yappy/macos/Runner/Base.lproj/MainMenu.xib b/team_b/yappy/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 80e867a4..00000000 --- a/team_b/yappy/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/team_b/yappy/macos/Runner/Configs/AppInfo.xcconfig b/team_b/yappy/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 7ca93a44..00000000 --- a/team_b/yappy/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = yappy - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.yappy - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/team_b/yappy/macos/Runner/Configs/Debug.xcconfig b/team_b/yappy/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd94..00000000 --- a/team_b/yappy/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/team_b/yappy/macos/Runner/Configs/Release.xcconfig b/team_b/yappy/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f495..00000000 --- a/team_b/yappy/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/team_b/yappy/macos/Runner/Configs/Warnings.xcconfig b/team_b/yappy/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf47..00000000 --- a/team_b/yappy/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/team_b/yappy/macos/Runner/DebugProfile.entitlements b/team_b/yappy/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a30..00000000 --- a/team_b/yappy/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/team_b/yappy/macos/Runner/Info.plist b/team_b/yappy/macos/Runner/Info.plist deleted file mode 100644 index 4789daa6..00000000 --- a/team_b/yappy/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/team_b/yappy/macos/Runner/MainFlutterWindow.swift b/team_b/yappy/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 3cc05eb2..00000000 --- a/team_b/yappy/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/team_b/yappy/macos/Runner/Release.entitlements b/team_b/yappy/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a4..00000000 --- a/team_b/yappy/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/team_b/yappy/macos/RunnerTests/RunnerTests.swift b/team_b/yappy/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 61f3bd1f..00000000 --- a/team_b/yappy/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Cocoa -import FlutterMacOS -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/team_b/yappy/pubspec.yaml b/team_b/yappy/pubspec.yaml deleted file mode 100644 index e5d11e00..00000000 --- a/team_b/yappy/pubspec.yaml +++ /dev/null @@ -1,120 +0,0 @@ -name: yappy -description: "Yappy! is an application that allows you to store transcripts in different industry contexts." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 - -environment: - sdk: ^3.6.2 - -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. -dependencies: - flutter: - sdk: flutter - - intl: ^0.17.0 - audio_waveforms: ^1.2.0 - cupertino_icons: ^1.0.8 - flutter_launcher_icons: ^0.14.3 - sqflite: ^2.4.1 - path: ^1.9.0 - path_provider: ^2.1.5 - envied: ^1.1.1 - sherpa_onnx: ^1.10.46 - record: ^6.0.0 - connectivity_plus: ^6.1.3 - shared_preferences: ^2.5.2 - share_plus: ^10.1.4 # Allows sharing with the phones default apps. - http: ^1.3.0 # For network requests - permission_handler: ^11.4.0 # For storage permissions - archive: ^4.0.4 # For BZ2 decompression - file_picker: ^9.0.2 - flutter_audio_waveforms: ^1.2.1+8 - dart_openai: ^5.1.0 - provider: ^6.1.4 - aws_common: ^0.7.6 - aws_signature_v4: ^0.6.4 - http2: ^2.3.1 - uuid: ^4.5.1 - convert: ^3.1.2 - crypto: ^3.0.6 - sqflite_common_ffi: ^2.3.5 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^5.0.0 - envied_generator: ^1.1.1 - build_runner: ^2.4.15 - mockito: ^5.0.16 - -flutter_icons: - android: true # For Android app icon - ios: true # For iOS app icon (optional, but recommended) - image_path: "assets/icon/app_icon.png" # Path to your PNG icon - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - assets: - - assets/icon/app_icon.png - - assets/yappy_database.db - - assets/models_config.json - - assets/ - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package diff --git a/team_b/yappy/test/audiowave_widget_test.dart b/team_b/yappy/test/audiowave_widget_test.dart deleted file mode 100644 index 86cfba10..00000000 --- a/team_b/yappy/test/audiowave_widget_test.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:yappy/audiowave_widget.dart'; -import 'package:yappy/services/speech_state.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('AudiowaveWidget displays correctly', - (WidgetTester tester) async { - // Mock SpeechState - final mockSpeechState = SpeechState(); - mockSpeechState.audioSamplesNotifier.value = - List.generate(100, (index) => (index % 2 == 0) ? 1000 : -1000); - - // Build the AudiowaveWidget widget. - await tester.pumpWidget(MaterialApp( - home: Scaffold( - body: AudiowaveWidget(speechState: mockSpeechState), - ), - )); - - // Allow the widget to fully build. - await tester.pumpAndSettle(); - - // Verify that the AudiowaveWidget is displayed. - expect(find.byType(AudiowaveWidget), findsOneWidget); - - // Verify that the CustomPaint widget is present. - expect(find.byType(CustomPaint), findsNWidgets(2)); - }); - - testWidgets('AudiowaveWidget updates with new audio samples', - (WidgetTester tester) async { - // Mock SpeechState - final mockSpeechState = SpeechState(); - mockSpeechState.audioSamplesNotifier.value = - List.generate(100, (index) => (index % 2 == 0) ? 1000 : -1000); - - // Build the AudiowaveWidget widget. - await tester.pumpWidget(MaterialApp( - home: Scaffold( - body: AudiowaveWidget(speechState: mockSpeechState), - ), - )); - - // Allow the widget to fully build. - await tester.pumpAndSettle(); - - // Update the audio samples. - mockSpeechState.audioSamplesNotifier.value = - List.generate(100, (index) => (index % 2 == 0) ? 2000 : -2000); - await tester.pumpAndSettle(); - - // Verify that the CustomPaint widget is updated. - expect(find.byType(CustomPaint), findsNWidgets(2)); - }); -} diff --git a/team_b/yappy/test/contact_page_test.dart b/team_b/yappy/test/contact_page_test.dart deleted file mode 100644 index c82f1c83..00000000 --- a/team_b/yappy/test/contact_page_test.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:yappy/contact_page.dart'; -import 'package:yappy/tool_bar.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('ContactPage should have a ToolBar and HamburgerDrawer', - (WidgetTester tester) async { - // Build the widget - await tester.pumpWidget(MaterialApp(home: ContactPage())); - - // Verify that ToolBar is in place - expect(find.byType(ToolBar), findsOneWidget, - reason: 'ToolBar should be visible'); - - // Find the Scaffold widget and trigger openDrawer() on its context - final scaffoldFinder = find.byType(Scaffold); - - // Use the `tester` to access the Scaffold and open the drawer directly - final scaffoldState = tester.state(scaffoldFinder); - scaffoldState.openDrawer(); // This explicitly opens the drawer - - await tester.pumpAndSettle(); // Wait for the drawer to open - - // Verify that the HamburgerDrawer is visible after opening - expect(find.byType(HamburgerDrawer), findsOneWidget, - reason: 'HamburgerDrawer should be available'); - }); - - testWidgets('Tapping the menu button should open the drawer', - (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: ContactPage())); - - // Drawer should not be visible initially - expect(find.byType(Drawer), findsNothing); - - // Tap the menu button (leading icon in AppBar) - await tester.tap(find.byIcon(Icons.menu)); - await tester.pumpAndSettle(); // Wait for animations - - // Drawer should be visible now - expect(find.byType(Drawer), findsOneWidget); - }); -} diff --git a/team_b/yappy/test/database_helper.mocks.dart b/team_b/yappy/test/database_helper.mocks.dart deleted file mode 100644 index 768fd8e1..00000000 --- a/team_b/yappy/test/database_helper.mocks.dart +++ /dev/null @@ -1,514 +0,0 @@ -// Mocks generated by Mockito 5.4.5 from annotations -// in yappy/test/database_helper.mocks.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:sqflite/sqflite.dart' as _i2; -import 'package:yappy/services/database_helper.dart' as _i3; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: must_be_immutable -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -class _FakeDatabase_0 extends _i1.SmartFake implements _i2.Database { - _FakeDatabase_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -/// A class which mocks [DatabaseHelper]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockDatabaseHelper extends _i1.Mock implements _i3.DatabaseHelper { - MockDatabaseHelper() { - _i1.throwOnMissingStub(this); - } - - @override - _i4.Future<_i2.Database> get database => - (super.noSuchMethod( - Invocation.getter(#database), - returnValue: _i4.Future<_i2.Database>.value( - _FakeDatabase_0(this, Invocation.getter(#database)), - ), - ) - as _i4.Future<_i2.Database>); - - @override - _i4.Future>> getUsers() => - (super.noSuchMethod( - Invocation.method(#getUsers, []), - returnValue: _i4.Future>>.value( - >[], - ), - ) - as _i4.Future>>); - - @override - _i4.Future insertUser(Map? user) => - (super.noSuchMethod( - Invocation.method(#insertUser, [user]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future updateUser(Map? user) => - (super.noSuchMethod( - Invocation.method(#updateUser, [user]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future deleteUser(int? id) => - (super.noSuchMethod( - Invocation.method(#deleteUser, [id]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future insertVehicle(Map? vehicle) => - (super.noSuchMethod( - Invocation.method(#insertVehicle, [vehicle]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future updateVehicle(Map? vehicle) => - (super.noSuchMethod( - Invocation.method(#updateVehicle, [vehicle]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future deleteVehicle(int? id) => - (super.noSuchMethod( - Invocation.method(#deleteVehicle, [id]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future insertVehicleMaintenance(Map? maintenance) => - (super.noSuchMethod( - Invocation.method(#insertVehicleMaintenance, [maintenance]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future updateVehicleMaintenance(Map? maintenance) => - (super.noSuchMethod( - Invocation.method(#updateVehicleMaintenance, [maintenance]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future deleteVehicleMaintenance(int? id) => - (super.noSuchMethod( - Invocation.method(#deleteVehicleMaintenance, [id]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future insertTranscript(Map? transcript) => - (super.noSuchMethod( - Invocation.method(#insertTranscript, [transcript]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future updateTranscript(Map? transcript) => - (super.noSuchMethod( - Invocation.method(#updateTranscript, [transcript]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future deleteTranscript(int? id) => - (super.noSuchMethod( - Invocation.method(#deleteTranscript, [id]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future>> getAllTranscripts() => - (super.noSuchMethod( - Invocation.method(#getAllTranscripts, []), - returnValue: _i4.Future>>.value( - >[], - ), - ) - as _i4.Future>>); - - @override - _i4.Future?> getTranscriptById(int? transcriptId) => - (super.noSuchMethod( - Invocation.method(#getTranscriptById, [transcriptId]), - returnValue: _i4.Future?>.value(), - ) - as _i4.Future?>); - - @override - _i4.Future insertOrder(Map? order) => - (super.noSuchMethod( - Invocation.method(#insertOrder, [order]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future updateOrder(Map? order) => - (super.noSuchMethod( - Invocation.method(#updateOrder, [order]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future deleteOrder(int? id) => - (super.noSuchMethod( - Invocation.method(#deleteOrder, [id]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future insertOrderItem(Map? orderItem) => - (super.noSuchMethod( - Invocation.method(#insertOrderItem, [orderItem]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future updateOrderItem(Map? orderItem) => - (super.noSuchMethod( - Invocation.method(#updateOrderItem, [orderItem]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future deleteOrderItem(int? id) => - (super.noSuchMethod( - Invocation.method(#deleteOrderItem, [id]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future>> getMenuItems() => - (super.noSuchMethod( - Invocation.method(#getMenuItems, []), - returnValue: _i4.Future>>.value( - >[], - ), - ) - as _i4.Future>>); - - @override - _i4.Future insertMenuItem(Map? menuItem) => - (super.noSuchMethod( - Invocation.method(#insertMenuItem, [menuItem]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future updateMenuItem(Map? menuItem) => - (super.noSuchMethod( - Invocation.method(#updateMenuItem, [menuItem]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future deleteMenuItem(int? id) => - (super.noSuchMethod( - Invocation.method(#deleteMenuItem, [id]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future insertSpecialRequest(Map? specialRequest) => - (super.noSuchMethod( - Invocation.method(#insertSpecialRequest, [specialRequest]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future updateSpecialRequest(Map? specialRequest) => - (super.noSuchMethod( - Invocation.method(#updateSpecialRequest, [specialRequest]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future deleteSpecialRequest(int? id) => - (super.noSuchMethod( - Invocation.method(#deleteSpecialRequest, [id]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future>> getDoctorVisits() => - (super.noSuchMethod( - Invocation.method(#getDoctorVisits, []), - returnValue: _i4.Future>>.value( - >[], - ), - ) - as _i4.Future>>); - - @override - _i4.Future insertDoctorVisit(Map? doctorVisit) => - (super.noSuchMethod( - Invocation.method(#insertDoctorVisit, [doctorVisit]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future updateDoctorVisit(Map? doctorVisit) => - (super.noSuchMethod( - Invocation.method(#updateDoctorVisit, [doctorVisit]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future deleteDoctorVisit(int? id) => - (super.noSuchMethod( - Invocation.method(#deleteDoctorVisit, [id]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future insertPatient(Map? patient) => - (super.noSuchMethod( - Invocation.method(#insertPatient, [patient]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future updatePatient(Map? patient) => - (super.noSuchMethod( - Invocation.method(#updatePatient, [patient]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future deletePatient(int? id) => - (super.noSuchMethod( - Invocation.method(#deletePatient, [id]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future?> getDoctorVisitById(int? visitId) => - (super.noSuchMethod( - Invocation.method(#getDoctorVisitById, [visitId]), - returnValue: _i4.Future?>.value(), - ) - as _i4.Future?>); - - @override - _i4.Future>> getDoctorVisitsByPatientId( - int? patientId, - ) => - (super.noSuchMethod( - Invocation.method(#getDoctorVisitsByPatientId, [patientId]), - returnValue: _i4.Future>>.value( - >[], - ), - ) - as _i4.Future>>); - - @override - _i4.Future?> getVehicleMaintenanceById( - int? maintenanceId, - ) => - (super.noSuchMethod( - Invocation.method(#getVehicleMaintenanceById, [maintenanceId]), - returnValue: _i4.Future?>.value(), - ) - as _i4.Future?>); - - @override - _i4.Future?> getVehicleById(int? vehicleId) => - (super.noSuchMethod( - Invocation.method(#getVehicleById, [vehicleId]), - returnValue: _i4.Future?>.value(), - ) - as _i4.Future?>); - - @override - _i4.Future> getVehicleIdsByUserId(int? userId) => - (super.noSuchMethod( - Invocation.method(#getVehicleIdsByUserId, [userId]), - returnValue: _i4.Future>.value([]), - ) - as _i4.Future>); - - @override - _i4.Future> getTranscriptIdsByUserId(int? userId) => - (super.noSuchMethod( - Invocation.method(#getTranscriptIdsByUserId, [userId]), - returnValue: _i4.Future>.value([]), - ) - as _i4.Future>); - - @override - _i4.Future> getMaintenanceIdsByUserId(int? userId) => - (super.noSuchMethod( - Invocation.method(#getMaintenanceIdsByUserId, [userId]), - returnValue: _i4.Future>.value([]), - ) - as _i4.Future>); - - @override - _i4.Future> getItemIdsByOrderId(int? orderId) => - (super.noSuchMethod( - Invocation.method(#getItemIdsByOrderId, [orderId]), - returnValue: _i4.Future>.value([]), - ) - as _i4.Future>); - - @override - dynamic saveTranscript({ - required int? userId, - required int? transcriptId, - required String? text, - required String? industry, - }) => super.noSuchMethod( - Invocation.method(#saveTranscript, [], { - #userId: userId, - #transcriptId: transcriptId, - #text: text, - #industry: industry, - }), - ); - - @override - dynamic saveTranscriptAiResponse({ - required int? userId, - required int? transcriptId, - required String? text, - required String? aiResponse, - required String? industry, - }) => super.noSuchMethod( - Invocation.method(#saveTranscriptAiResponse, [], { - #userId: userId, - #transcriptId: transcriptId, - #text: text, - #aiResponse: aiResponse, - #industry: industry, - }), - ); - - @override - _i4.Future> searchTranscripts(String? query, String? industry) => - (super.noSuchMethod( - Invocation.method(#searchTranscripts, [query, industry]), - returnValue: _i4.Future>.value([]), - ) - as _i4.Future>); - - @override - _i4.Future?> getTranscriptDetails(String? entry) => - (super.noSuchMethod( - Invocation.method(#getTranscriptDetails, [entry]), - returnValue: _i4.Future?>.value(), - ) - as _i4.Future?>); - - @override - _i4.Future insertDocument( - int? transcriptId, - String? fileName, - List? fileBytes, - ) => - (super.noSuchMethod( - Invocation.method(#insertDocument, [ - transcriptId, - fileName, - fileBytes, - ]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future?> getTranscriptTextDataAndIndustryById( - int? transcriptId, - ) => - (super.noSuchMethod( - Invocation.method(#getTranscriptTextDataAndIndustryById, [ - transcriptId, - ]), - returnValue: _i4.Future?>.value(), - ) - as _i4.Future?>); - - @override - _i4.Future?> getDocumentAndIndustryById( - int? transcriptId, - ) => - (super.noSuchMethod( - Invocation.method(#getDocumentAndIndustryById, [transcriptId]), - returnValue: _i4.Future?>.value(), - ) - as _i4.Future?>); - - @override - _i4.Future insertAiResponse(int? transcriptId, String? aiResponse) => - (super.noSuchMethod( - Invocation.method(#insertAiResponse, [transcriptId, aiResponse]), - returnValue: _i4.Future.value(0), - ) - as _i4.Future); - - @override - _i4.Future updateTranscriptDocument( - int? transcriptId, - List? documentBytes, - ) => - (super.noSuchMethod( - Invocation.method(#updateTranscriptDocument, [ - transcriptId, - documentBytes, - ]), - returnValue: _i4.Future.value(false), - ) - as _i4.Future); -} diff --git a/team_b/yappy/test/database_helper_test.dart b/team_b/yappy/test/database_helper_test.dart deleted file mode 100644 index c1530336..00000000 --- a/team_b/yappy/test/database_helper_test.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'database_helper.mocks.dart'; - -void main() { - late MockDatabaseHelper mockDataHelper; - - setUp(() { - mockDataHelper = MockDatabaseHelper(); - }); - - group('DatabaseHelper Tests', () { - test('should get transcript text data and industry by ID', () async { - int transcriptId = 1; - Map mockTranscriptData = { - 'transcript_text_data': 'Sample text', - 'industry': 'Sample industry', - }; - - when(mockDataHelper.getTranscriptTextDataAndIndustryById(transcriptId)) - .thenAnswer((_) async => mockTranscriptData); - - final transcriptData = await mockDataHelper.getTranscriptTextDataAndIndustryById(transcriptId); - expect(transcriptData, isNotNull); - expect(transcriptData!['transcript_text_data'], 'Sample text'); - expect(transcriptData['industry'], 'Sample industry'); - }); - - test('should update transcript document', () async { - int transcriptId = 1; - List documentBytes = [1, 2, 3, 4, 5]; - - when(mockDataHelper.updateTranscriptDocument(transcriptId, documentBytes)) - .thenAnswer((_) async => true); - - final result = await mockDataHelper.updateTranscriptDocument(transcriptId, documentBytes); - expect(result, true); - }); - - test('should get transcript by ID', () async { - int transcriptId = 1; - Map mockTranscript = { - 'transcript_id': transcriptId, - 'transcript_text_data': 'Sample text', - 'industry': 'Sample industry', - }; - - when(mockDataHelper.getTranscriptById(transcriptId)) - .thenAnswer((_) async => mockTranscript); - - final transcript = await mockDataHelper.getTranscriptById(transcriptId); - expect(transcript, isNotNull); - expect(transcript!['transcript_id'], transcriptId); - }); - - test('should insert AI response', () async { - int transcriptId = 1; - String aiResponse = 'This is an AI response'; - - when(mockDataHelper.insertAiResponse(transcriptId, aiResponse)) - .thenAnswer((_) async => 1); - - final result = await mockDataHelper.insertAiResponse(transcriptId, aiResponse); - expect(result, 1); - }); - }); -} \ No newline at end of file diff --git a/team_b/yappy/test/file_handler_test.dart b/team_b/yappy/test/file_handler_test.dart deleted file mode 100644 index f2a7d42e..00000000 --- a/team_b/yappy/test/file_handler_test.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:yappy/services/file_handler.dart'; -import 'dart:io'; -import 'package:flutter/services.dart'; -import 'database_helper.mocks.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - late MockDatabaseHelper mockDbHelper; - late FileHandler fileHandler; - const MethodChannel pathProviderChannel = MethodChannel('plugins.flutter.io/path_provider'); - // I have to cache the temp directory path as multiple calls - // to getApplicationDocumentsDirectory() return different paths - late String tempDirectoryPath; - - setUpAll(() async { - // Register the path_provider plugin - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( - pathProviderChannel, - (MethodCall methodCall) async { - if (methodCall.method == 'getApplicationDocumentsDirectory') { - final directory = Directory.systemTemp.createTempSync(); - tempDirectoryPath = directory.path; - return tempDirectoryPath; - } - return null; - } - ); - - // Initialize the database - mockDbHelper = MockDatabaseHelper(); - fileHandler = FileHandler(); - }); - - group('FileHandler', () { - setUp(() { - mockDbHelper = MockDatabaseHelper(); - fileHandler = FileHandler(); - }); - - test('should save transcript text data to local storage', () async { - int transcriptId = 1; // Assuming transcriptId 1 exists in the database with test data - Map mockTranscriptData = { - 'transcript_text_data': 'Sample text', - 'industry': 'Sample industry', - }; - - when(mockDbHelper.getTranscriptTextDataAndIndustryById(transcriptId)) - .thenAnswer((_) async => mockTranscriptData); - - await fileHandler.saveTranscriptTextToLocal(mockDbHelper, transcriptId); - final directory = tempDirectoryPath; - final fileName = 'transcript_text_${transcriptId}_Sample industry.txt'; - final file = File('$directory/$fileName'); - - expect(await file.exists(), isTrue); - }); - - test('should move file from local storage to database', () async { - int transcriptId = 1; // Assuming transcriptId 1 exists in the database with test data - final fileName = 'transcript_text_${transcriptId}_Vehicle Maintenance.txt'; - final directory = await fileHandler.localStoragePath; - final file = File('$directory/$fileName'); - - // Setup: Create the file in local storage - await file.writeAsString('Test content for transcript text'); - - if (await file.exists()) { - final fileBytes = await file.readAsBytes(); - when(mockDbHelper.updateTranscriptDocument(transcriptId, fileBytes)) - .thenAnswer((_) async => true); - when(mockDbHelper.getTranscriptById(transcriptId)) - .thenAnswer((_) async => { - 'transcript_id': transcriptId, - 'transcript_document': fileBytes, - }); - - await mockDbHelper.updateTranscriptDocument(transcriptId, fileBytes); - - final transcript = await mockDbHelper.getTranscriptById(transcriptId); - expect(transcript, isNotNull); - expect(transcript!['transcript_document'], fileBytes); - } else { - fail('File not found in local storage: $fileName'); - } - }); - }); -} \ No newline at end of file diff --git a/team_b/yappy/test/help_test.dart b/team_b/yappy/test/help_test.dart deleted file mode 100644 index 58e3907b..00000000 --- a/team_b/yappy/test/help_test.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:yappy/help.dart'; -import 'package:yappy/tutorial_page.dart'; - -void main() { - group('HelpApp Widget Tests', () { - testWidgets('HelpApp renders HelpPage as the home screen', - (WidgetTester tester) async { - await tester.pumpWidget(const HelpApp()); - - expect(find.byType(HelpPage), findsOneWidget); - }); - }); - - group('HelpPage Widget Tests', () { - testWidgets('HelpPage has the correct background color', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: HelpPage(), - ), - ); - - final scaffold = tester.widget(find.byType(Scaffold)); - expect(scaffold.backgroundColor, const Color.fromARGB(255, 0, 0, 0)); - }); - - testWidgets('HelpPage displays the "Lets Yap about Yappy" title', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: HelpPage(), - ), - ); - - expect(find.text('Lets Yap about Yappy'), findsOneWidget); - }); - - testWidgets('HelpPage displays the welcome message', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: HelpPage(), - ), - ); - - expect( - find.text( - 'Welcome to Yappy! If this is your first time and need help with using Yappy, please select the button below.', - ), - findsOneWidget, - ); - }); - - testWidgets( - 'HelpPage displays "It\'s my first time" button and navigates to TutorialPage', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: HelpPage(), - ), - ); - - expect(find.text('It\'s my first time'), findsOneWidget); - - await tester.tap(find.text('It\'s my first time')); - await tester.pumpAndSettle(); - - expect(find.byType(TutorialPage), findsOneWidget); - }); - - testWidgets( - 'HelpPage displays "Report a problem" button and shows alert dialog', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: HelpPage(), - ), - ); - - expect(find.text('Report a problem'), findsOneWidget); - - await tester.tap(find.text('Report a problem')); - await tester.pump(); - - expect(find.text('Report a Problem'), findsOneWidget); - expect(find.text('Please call: +1-800-123-4567'), findsOneWidget); - }); - - testWidgets( - 'HelpPage displays "Feedback for the Help Center" button and shows alert dialog', - (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: HelpPage(), - ), - ); - - expect(find.text('Feedback for the Help Center'), findsOneWidget); - - await tester.tap(find.text('Feedback for the Help Center')); - await tester.pump(); - - expect(find.text('Feedback for the Help Center'), findsNWidgets(2)); - }); - }); -} diff --git a/team_b/yappy/test/home_page_test.dart b/team_b/yappy/test/home_page_test.dart deleted file mode 100644 index 62b9fbe6..00000000 --- a/team_b/yappy/test/home_page_test.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:yappy/home_page.dart'; -import 'package:yappy/tutorial_page.dart'; - -import 'package:shared_preferences/shared_preferences.dart'; - -void main() { - group('HomePage Tests', () { - testWidgets('displays all buttons on the HomePage', - (WidgetTester tester) async { - // Arrange - await tester.pumpWidget(MaterialApp(home: HomePage())); - - // Assert - expect(find.text('Restaurant'), findsOneWidget); - expect(find.text('Vehicle Maintenance'), findsOneWidget); - expect(find.text('Medical Doctor'), findsOneWidget); - expect(find.text('Medical Patient'), findsOneWidget); - expect(find.text('Help'), findsOneWidget); - expect(find.text('Contact'), findsOneWidget); - expect(find.text('Settings'), findsOneWidget); - }); - - testWidgets('navigates to TutorialPage on first-time user dialog "Yes"', - (WidgetTester tester) async { - // Arrange - SharedPreferences.setMockInitialValues({'isFirstTime': true}); - await tester.pumpWidget(MaterialApp(home: HomePage())); - - // Act - await tester.pump(); - await tester.tap(find.text('Yes')); - await tester.pumpAndSettle(); - - // Assert - expect(find.byType(TutorialPage), findsOneWidget); - }); - - testWidgets( - 'does not show first-time user dialog if "isFirstTime" is false', - (WidgetTester tester) async { - // Arrange - SharedPreferences.setMockInitialValues({'isFirstTime': false}); - await tester.pumpWidget(MaterialApp(home: HomePage())); - - // Assert - expect(find.text('Welcome!'), findsNothing); - }); - }); -} diff --git a/team_b/yappy/test/login_page_test.dart b/team_b/yappy/test/login_page_test.dart deleted file mode 100644 index 7b52fde1..00000000 --- a/team_b/yappy/test/login_page_test.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:yappy/login_page.dart'; -import 'package:yappy/sign_up_page.dart'; - -void main() { - group('LoginPage Tests', () { - testWidgets('renders all widgets properly', (WidgetTester tester) async { - // Arrange - await tester.pumpWidget(MaterialApp(home: LoginPage())); - - // Assert - expect(find.byType(CircleAvatar), findsOneWidget); - expect(find.text('Login'), findsOneWidget); - expect(find.text('Submit'), findsOneWidget); - expect(find.text('Sign-up'), findsOneWidget); - expect(find.text('Terms and Conditions'), findsOneWidget); - }); - - testWidgets('navigates to SignUpPage when "Sign-up" button is pressed', - (WidgetTester tester) async { - // Arrange - await tester.pumpWidget(MaterialApp(home: LoginPage())); - - // Act - await tester.tap(find.text('Sign-up')); - await tester.pumpAndSettle(); - - // Assert - expect(find.byType(SignUpPage), findsOneWidget); - }); - - testWidgets('ensures TextFields update with input', - (WidgetTester tester) async { - // Arrange - await tester.pumpWidget(MaterialApp(home: LoginPage())); - - // Act - await tester.enterText( - find.widgetWithText(TextField, 'Username'), 'testuser'); - await tester.enterText( - find.widgetWithText(TextField, 'Password'), 'password123'); - - // Assert - final usernameField = find.widgetWithText(TextField, 'Username'); - final passwordField = find.widgetWithText(TextField, 'Password'); - - expect((tester.widget(usernameField) as TextField).controller?.text, - 'testuser'); - expect((tester.widget(passwordField) as TextField).controller?.text, - 'password123'); - }); - }); -} diff --git a/team_b/yappy/test/main_test.dart b/team_b/yappy/test/main_test.dart deleted file mode 100644 index b4329ed9..00000000 --- a/team_b/yappy/test/main_test.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:yappy/main.dart'; -import 'package:yappy/home_page.dart'; - -void main() { - group('Main Tests', () { - testWidgets('renders MyApp and HomePage correctly', - (WidgetTester tester) async { - // Arrange - SharedPreferences.setMockInitialValues({}); // Mock shared preferences - await tester.pumpWidget(const MyApp()); - - // Assert - expect(find.byType(MyApp), findsOneWidget); - expect(find.byType(HomePage), findsOneWidget); - }); - - testWidgets('displays API key alert dialog when API key is empty', - (WidgetTester tester) async { - // Arrange - SharedPreferences.setMockInitialValues({'openai_api_key': ''}); - final navigatorKey = GlobalKey(); - - await tester.pumpWidget( - MaterialApp( - navigatorKey: navigatorKey, - home: Builder(builder: (context) { - WidgetsBinding.instance.addPostFrameCallback((_) { - showDialog( - context: navigatorKey.currentContext!, - builder: (BuildContext context) { - return AlertDialog( - title: Text('OpenAI API Key Required'), - content: Text( - 'Please add a valid OpenAI API key via the Settings menu.'), - actions: [ - TextButton( - child: Text('OK'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ); - }); - return Container(); - }), - ), - ); - - // Act - await tester.pumpAndSettle(); - - // Assert - expect(find.text('OpenAI API Key Required'), findsOneWidget); - expect( - find.text('Please add a valid OpenAI API key via the Settings menu.'), - findsOneWidget); - }); - - test('checks API key is saved in shared preferences', () async { - // Arrange - SharedPreferences.setMockInitialValues({}); - final preferences = await SharedPreferences.getInstance(); - - // Act - preferences.setString('openai_api_key', 'test_api_key'); - - // Assert - expect(preferences.getString('openai_api_key'), 'test_api_key'); - }); - }); -} diff --git a/team_b/yappy/test/mechanic_test.dart b/team_b/yappy/test/mechanic_test.dart deleted file mode 100644 index 03f424d4..00000000 --- a/team_b/yappy/test/mechanic_test.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:yappy/audiowave_widget.dart'; -import 'package:yappy/industry_menu.dart'; -import 'package:yappy/mechanic.dart'; -import 'package:yappy/search_bar_widget.dart'; -import 'package:yappy/tool_bar.dart'; -import 'package:yappy/transcription_box.dart'; - -void main() { - testWidgets('MechanicalAidApp should have a MechanicalAidPage', (WidgetTester tester) async { - await tester.pumpWidget(const MechanicalAidApp()); - - expect(find.byType(MechanicalAidPage), findsOneWidget); - }); - - testWidgets('MechanicalAidPage should have a ToolBar', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: MechanicalAidPage())); - - expect(find.byType(ToolBar), findsOneWidget); - }); - - testWidgets('MechanicalAidPage should have a SearchBarWidget and IndustryMenu', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: MechanicalAidPage())); - - expect(find.byType(SearchBarWidget), findsOneWidget); - expect(find.byType(IndustryMenu), findsOneWidget); - }); - - testWidgets('MechanicalAidPage should have AudiowaveWidget and TranscriptionBox', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: MechanicalAidPage())); - - expect(find.byType(AudiowaveWidget), findsOneWidget); - expect(find.byType(TranscriptionBox), findsOneWidget); - }); -} \ No newline at end of file diff --git a/team_b/yappy/test/medical_doctor.dart b/team_b/yappy/test/medical_doctor.dart deleted file mode 100644 index 2ddf87cf..00000000 --- a/team_b/yappy/test/medical_doctor.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:yappy/audiowave_widget.dart'; -import 'package:yappy/industry_menu.dart'; -import 'package:yappy/medical_doctor.dart'; -import 'package:yappy/search_bar_widget.dart'; -import 'package:yappy/tool_bar.dart'; -import 'package:yappy/transcription_box.dart'; - -void main() { - testWidgets('MedicalDoctorApp should have a MedicalDoctorPage', (WidgetTester tester) async { - await tester.pumpWidget(const MedicalDoctorApp()); - - expect(find.byType(MedicalDoctorPage), findsOneWidget); - }); - - testWidgets('MedicalDoctorPage should have a ToolBar', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: MedicalDoctorPage())); - - expect(find.byType(ToolBar), findsOneWidget); - }); - - testWidgets('MedicalDoctorPage should have a SearchBarWidget and IndustryMenu', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: MedicalDoctorPage())); - - expect(find.byType(SearchBarWidget), findsOneWidget); - expect(find.byType(IndustryMenu), findsOneWidget); - }); - - testWidgets('MedicalDoctorPage should have AudiowaveWidget and TranscriptionBox', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: MedicalDoctorPage())); - - expect(find.byType(AudiowaveWidget), findsOneWidget); - expect(find.byType(TranscriptionBox), findsOneWidget); - }); -} \ No newline at end of file diff --git a/team_b/yappy/test/medical_doctor_test.dart b/team_b/yappy/test/medical_doctor_test.dart deleted file mode 100644 index 53eef6c8..00000000 --- a/team_b/yappy/test/medical_doctor_test.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:yappy/medical_doctor.dart'; -import 'package:yappy/industry_menu.dart'; -import 'package:yappy/transcription_box.dart'; -import 'package:yappy/audiowave_widget.dart'; - -void main() { - group('MedicalDoctorPage Tests', () { - testWidgets('renders MedicalDoctorApp and MedicalDoctorPage correctly', - (WidgetTester tester) async { - // Arrange - await tester.pumpWidget(const MedicalDoctorApp()); - - // Assert - expect(find.byType(MedicalDoctorApp), findsOneWidget); - expect(find.byType(MedicalDoctorPage), findsOneWidget); - }); - - testWidgets('renders IndustryMenu with correct title and icon', - (WidgetTester tester) async { - // Arrange - await tester.pumpWidget(MaterialApp(home: MedicalDoctorPage())); - - // Act - await tester.pumpAndSettle(); - - // Assert - expect(find.byType(IndustryMenu), findsOneWidget); - expect( - find.widgetWithText(IndustryMenu, 'Medical Doctor'), findsOneWidget); - expect(find.byIcon(Icons.medical_services), findsOneWidget); - }); - - testWidgets('renders AudiowaveWidget and TranscriptionBox', - (WidgetTester tester) async { - // Arrange - await tester.pumpWidget(MaterialApp(home: MedicalDoctorPage())); - - // Act - await tester.pumpAndSettle(); - - // Assert - expect(find.byType(AudiowaveWidget), findsOneWidget); - expect(find.byType(TranscriptionBox), findsOneWidget); - }); - }); -} diff --git a/team_b/yappy/test/medical_patient_test.dart b/team_b/yappy/test/medical_patient_test.dart deleted file mode 100644 index 24d32b77..00000000 --- a/team_b/yappy/test/medical_patient_test.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:yappy/audiowave_widget.dart'; -import 'package:yappy/industry_menu.dart'; -import 'package:yappy/medical_patient.dart'; -import 'package:yappy/search_bar_widget.dart'; -import 'package:yappy/tool_bar.dart'; -import 'package:yappy/transcription_box.dart'; - -void main() { - testWidgets('MedicalPatientApp should have a MedicalPatientPage', (WidgetTester tester) async { - await tester.pumpWidget(const MedicalPatientApp()); - - expect(find.byType(MedicalPatientPage), findsOneWidget); - }); - - testWidgets('MedicalPatientPage should have a ToolBar', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: MedicalPatientPage())); - - expect(find.byType(ToolBar), findsOneWidget); - }); - - testWidgets('MedicalPatientPage should have a SearchBarWidget and IndustryMenu', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: MedicalPatientPage())); - - expect(find.byType(SearchBarWidget), findsOneWidget); - expect(find.byType(IndustryMenu), findsOneWidget); - }); - - testWidgets('MedicalPatientPage should have AudiowaveWidget and TranscriptionBox', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: MedicalPatientPage())); - - expect(find.byType(AudiowaveWidget), findsOneWidget); - expect(find.byType(TranscriptionBox), findsOneWidget); - }); -} \ No newline at end of file diff --git a/team_b/yappy/test/restaurant_test.dart b/team_b/yappy/test/restaurant_test.dart deleted file mode 100644 index c5a1b527..00000000 --- a/team_b/yappy/test/restaurant_test.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:yappy/audiowave_widget.dart'; -import 'package:yappy/industry_menu.dart'; -import 'package:yappy/restaurant.dart'; -import 'package:yappy/search_bar_widget.dart'; -import 'package:yappy/tool_bar.dart'; -import 'package:yappy/transcription_box.dart'; - -void main() { - testWidgets('RestaurantApp should have a RestaurantPage', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: RestaurantPage())); - - expect(find.byType(RestaurantPage), findsOneWidget); - }); - - testWidgets('RestaurantPage should have a ToolBar', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: RestaurantPage())); - - expect(find.byType(ToolBar), findsOneWidget); - }); - - testWidgets('RestaurantPage should have a SearchBarWidget and IndustryMenu', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: RestaurantPage())); - - expect(find.byType(SearchBarWidget), findsOneWidget); - expect(find.byType(IndustryMenu), findsOneWidget); - }); - - testWidgets('RestaurantPage should have AudiowaveWidget and TranscriptionBox', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: RestaurantPage())); - - expect(find.byType(AudiowaveWidget), findsOneWidget); - expect(find.byType(TranscriptionBox), findsOneWidget); - }); -} \ No newline at end of file diff --git a/team_b/yappy/test/sign_up_page_test.dart b/team_b/yappy/test/sign_up_page_test.dart deleted file mode 100644 index ace17bb0..00000000 --- a/team_b/yappy/test/sign_up_page_test.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/material.dart'; -import 'package:yappy/sign_up_page.dart'; - -void main() { - group('SignUpPage Tests', () { - testWidgets('Should display all required fields', - (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp(home: SignUpPage())); - - // Verify presence of username, password, and re-enter password fields - expect(find.byType(TextField), findsNWidgets(3)); - expect(find.text('Username'), findsOneWidget); - expect(find.text('Password'), findsOneWidget); - expect(find.text('Re-Enter Password'), findsOneWidget); - }); - - testWidgets('Should display Sign-Up button', (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp(home: SignUpPage())); - - // Verify presence of Sign-Up button - expect(find.text('Sign-Up'), findsOneWidget); - expect(find.byType(ElevatedButton), findsWidgets); - }); - - testWidgets('Should display disclaimer text', (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp(home: SignUpPage())); - - // Verify disclaimer text is present - expect( - find.text( - 'Yappy! Is not responsible for any legal consequences due to the use of this application', - ), - findsOneWidget, - ); - }); - - testWidgets('Should input text into username field', - (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp(home: SignUpPage())); - - // Input text into username field - final usernameField = find.widgetWithText(TextField, 'Username'); - await tester.enterText(usernameField, 'testuser'); - - // Verify text is entered - expect(find.text('testuser'), findsOneWidget); - }); - - testWidgets('Should input text into password field', - (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp(home: SignUpPage())); - - // Input text into password field - final passwordField = find.widgetWithText(TextField, 'Password'); - await tester.enterText(passwordField, 'password123'); - - // Verify text is entered - expect(find.text('password123'), findsOneWidget); - }); - - testWidgets('Should input text into re-enter password field', - (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp(home: SignUpPage())); - - // Input text into re-enter password field - final reenterPasswordField = - find.widgetWithText(TextField, 'Re-Enter Password'); - await tester.enterText(reenterPasswordField, 'password123'); - - // Verify text is entered - expect(find.text('password123'), findsOneWidget); - }); - - testWidgets('Submit button is functional', (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp(home: SignUpPage())); - - // Find and tap the Submit button - final submitButton = find.widgetWithText(ElevatedButton, 'Submit'); - await tester.tap(submitButton); - - // Verify button press - expect(find.widgetWithText(ElevatedButton, 'Submit'), findsOneWidget); - }); - }); -} diff --git a/team_b/yappy/test/transcription_box_test.dart b/team_b/yappy/test/transcription_box_test.dart deleted file mode 100644 index e655d4de..00000000 --- a/team_b/yappy/test/transcription_box_test.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/material.dart'; -import 'package:yappy/transcription_box.dart'; - -void main() { - group('TranscriptionBox Tests', () { - late TextEditingController textEditingController; - - setUp(() { - textEditingController = TextEditingController(); - }); - - testWidgets('Should display the transcription box', - (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: TranscriptionBox(controller: textEditingController), - ), - ), - ); - - // Verify the transcription box is displayed - expect(find.byType(TextField), findsOneWidget); - expect(find.text('Transcription will appear here...'), findsOneWidget); - }); - - testWidgets('Should update text in transcription box', - (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: TranscriptionBox(controller: textEditingController), - ), - ), - ); - - // Set text in the controller - textEditingController.text = 'Sample transcription text'; - await tester.pumpAndSettle(); - - // Verify the text is displayed - expect(find.text('Sample transcription text'), findsOneWidget); - }); - - testWidgets('Should display scroll bar when content overflows', - (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: TranscriptionBox(controller: textEditingController), - ), - ), - ); - - // Add long text to simulate overflow - textEditingController.text = 'Line 1\n' * 50; - await tester.pumpAndSettle(); - - // Verify scroll bar is visible - expect(find.byType(Scrollbar), findsOneWidget); - }); - }); -} diff --git a/team_b/yappy/test/tutorial_page_test.dart b/team_b/yappy/test/tutorial_page_test.dart deleted file mode 100644 index af142a20..00000000 --- a/team_b/yappy/test/tutorial_page_test.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/material.dart'; -import 'package:yappy/tool_bar.dart'; -import 'package:yappy/industry_menu.dart'; -import 'package:yappy/transcription_box.dart'; -import 'package:yappy/tutorial_page.dart'; -import 'package:yappy/home_page.dart'; - -void main() { - group('TutorialPage Tests', () { - testWidgets('Should display ToolBar and Drawer', - (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: TutorialPage(), - ), - ); - - // Verify ToolBar is displayed - expect(find.byType(ToolBar), findsOneWidget); - - // Verify HamburgerDrawer is present - final hamburgerIcon = find.byIcon(Icons.menu); - await tester.tap(hamburgerIcon); - await tester.pumpAndSettle(); - expect(find.byType(Drawer), findsOneWidget); - }); - - testWidgets('Should show first tutorial popup on load', - (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: TutorialPage(), - ), - ); - - // Wait for post-frame callback to execute - await tester.pumpAndSettle(); - - // Verify first popup is shown - expect( - find.text( - "The button of the left is the Record button that allows you to record conversations and get a transcript in return."), - findsOneWidget, - ); - }); - - testWidgets('Should navigate through tutorial popups', - (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: TutorialPage(), - ), - ); - - // Wait for first popup - await tester.pumpAndSettle(); - expect( - find.text( - "The button of the left is the Record button that allows you to record conversations and get a transcript in return."), - findsOneWidget); - - // Tap Next on first popup - await tester.tap(find.text("Next")); - await tester.pumpAndSettle(); - expect( - find.text( - "The second button will show you the days transcripts with broken down into details."), - findsOneWidget); - - // Tap Next on second popup - await tester.tap(find.text("Next")); - await tester.pumpAndSettle(); - expect( - find.text( - "The third button will show you all transcripts and allow you to search, share, upload, download, and delete."), - findsOneWidget); - - // Tap Next on third popup - await tester.tap(find.text("Next")); - await tester.pumpAndSettle(); - expect( - find.text( - "The fourth button will bring you to a chatbot that can search your transcripts to provide you information."), - findsOneWidget); - - // Tap Finish on fourth popup - await tester.tap(find.text("Finish")); - await tester.pumpAndSettle(); - - // Verify navigation to HomePage - expect(find.byType(HomePage), findsOneWidget); - }); - - testWidgets('Should display IndustryMenu and TranscriptionBox', - (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: TutorialPage(), - ), - ); - - // Verify IndustryMenu is displayed - expect(find.byType(IndustryMenu), findsOneWidget); - - // Verify TranscriptionBox is displayed - expect(find.byType(TranscriptionBox), findsOneWidget); - }); - }); -} diff --git a/team_b/yappy/web/favicon.png b/team_b/yappy/web/favicon.png deleted file mode 100644 index 8aaa46ac..00000000 Binary files a/team_b/yappy/web/favicon.png and /dev/null differ diff --git a/team_b/yappy/web/icons/Icon-192.png b/team_b/yappy/web/icons/Icon-192.png deleted file mode 100644 index b749bfef..00000000 Binary files a/team_b/yappy/web/icons/Icon-192.png and /dev/null differ diff --git a/team_b/yappy/web/icons/Icon-512.png b/team_b/yappy/web/icons/Icon-512.png deleted file mode 100644 index 88cfd48d..00000000 Binary files a/team_b/yappy/web/icons/Icon-512.png and /dev/null differ diff --git a/team_b/yappy/web/icons/Icon-maskable-192.png b/team_b/yappy/web/icons/Icon-maskable-192.png deleted file mode 100644 index eb9b4d76..00000000 Binary files a/team_b/yappy/web/icons/Icon-maskable-192.png and /dev/null differ diff --git a/team_b/yappy/web/icons/Icon-maskable-512.png b/team_b/yappy/web/icons/Icon-maskable-512.png deleted file mode 100644 index d69c5669..00000000 Binary files a/team_b/yappy/web/icons/Icon-maskable-512.png and /dev/null differ diff --git a/team_b/yappy/web/index.html b/team_b/yappy/web/index.html deleted file mode 100644 index 5756468d..00000000 --- a/team_b/yappy/web/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - yappy - - - - - - diff --git a/team_b/yappy/web/manifest.json b/team_b/yappy/web/manifest.json deleted file mode 100644 index d4e0ed7f..00000000 --- a/team_b/yappy/web/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "yappy", - "short_name": "yappy", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} diff --git a/team_b/yappy/windows/.gitignore b/team_b/yappy/windows/.gitignore deleted file mode 100644 index 4c0ca135..00000000 --- a/team_b/yappy/windows/.gitignore +++ /dev/null @@ -1,39 +0,0 @@ -flutter/ephemeral/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Ignore Flutter/Dart build artifacts -# Flutter build directories -**/build/ -**/flutter/GeneratedPluginRegistrant.* -**/flutter/generated_plugin_registrant.* -**/flutter/generated_plugins.cmake - -# Windows, macOS, Linux platform-specific build files -flutter_windows.dll.pdb -flutter_windows.dll -**/*.so -**/*.dylib -**/*.a -*.flutter-plugins -*.flutter-plugins-dependencies - -# Ignore any temporary or log files -*.log -*.tmp -*.swp -*.bak \ No newline at end of file diff --git a/team_b/yappy/windows/CMakeLists.txt b/team_b/yappy/windows/CMakeLists.txt deleted file mode 100644 index d8e98e42..00000000 --- a/team_b/yappy/windows/CMakeLists.txt +++ /dev/null @@ -1,108 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.14) -project(yappy LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "yappy") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(VERSION 3.14...3.25) - -# Define build configuration option. -get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) -if(IS_MULTICONFIG) - set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" - CACHE STRING "" FORCE) -else() - if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") - endif() -endif() -# Define settings for the Profile build mode. -set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") -set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") -set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") -set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") - -# Use Unicode for all projects. -add_definitions(-DUNICODE -D_UNICODE) - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_17) - target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") - target_compile_options(${TARGET} PRIVATE /EHsc) - target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") - target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# Support files are copied into place next to the executable, so that it can -# run in place. This is done instead of making a separate bundle (as on Linux) -# so that building and running from within Visual Studio will work. -set(BUILD_BUNDLE_DIR "$") -# Make the "install" step default, as it's required to run. -set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - CONFIGURATIONS Profile;Release - COMPONENT Runtime) diff --git a/team_b/yappy/windows/flutter/CMakeLists.txt b/team_b/yappy/windows/flutter/CMakeLists.txt deleted file mode 100644 index 903f4899..00000000 --- a/team_b/yappy/windows/flutter/CMakeLists.txt +++ /dev/null @@ -1,109 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.14) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. -set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") - -# Set fallback configurations for older versions of the flutter tool. -if (NOT DEFINED FLUTTER_TARGET_PLATFORM) - set(FLUTTER_TARGET_PLATFORM "windows-x64") -endif() - -# === Flutter Library === -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "flutter_export.h" - "flutter_windows.h" - "flutter_messenger.h" - "flutter_plugin_registrar.h" - "flutter_texture_registrar.h" -) -list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") -add_dependencies(flutter flutter_assemble) - -# === Wrapper === -list(APPEND CPP_WRAPPER_SOURCES_CORE - "core_implementations.cc" - "standard_codec.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_PLUGIN - "plugin_registrar.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_APP - "flutter_engine.cc" - "flutter_view_controller.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") - -# Wrapper sources needed for a plugin. -add_library(flutter_wrapper_plugin STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} -) -apply_standard_settings(flutter_wrapper_plugin) -set_target_properties(flutter_wrapper_plugin PROPERTIES - POSITION_INDEPENDENT_CODE ON) -set_target_properties(flutter_wrapper_plugin PROPERTIES - CXX_VISIBILITY_PRESET hidden) -target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) -target_include_directories(flutter_wrapper_plugin PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_plugin flutter_assemble) - -# Wrapper sources needed for the runner. -add_library(flutter_wrapper_app STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_APP} -) -apply_standard_settings(flutter_wrapper_app) -target_link_libraries(flutter_wrapper_app PUBLIC flutter) -target_include_directories(flutter_wrapper_app PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_app flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") -set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} - ${PHONY_OUTPUT} - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - ${FLUTTER_TARGET_PLATFORM} $ - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} -) diff --git a/team_b/yappy/windows/runner/CMakeLists.txt b/team_b/yappy/windows/runner/CMakeLists.txt deleted file mode 100644 index 394917c0..00000000 --- a/team_b/yappy/windows/runner/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} WIN32 - "flutter_window.cpp" - "main.cpp" - "utils.cpp" - "win32_window.cpp" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" - "Runner.rc" - "runner.exe.manifest" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the build version. -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") - -# Disable Windows macros that collide with C++ standard library functions. -target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") - -# Add dependency libraries and include directories. Add any application-specific -# dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) -target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/team_b/yappy/windows/runner/Runner.rc b/team_b/yappy/windows/runner/Runner.rc deleted file mode 100644 index 308bfff0..00000000 --- a/team_b/yappy/windows/runner/Runner.rc +++ /dev/null @@ -1,121 +0,0 @@ -// Microsoft Visual C++ generated resource script. -// -#pragma code_page(65001) -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (United States) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_APP_ICON ICON "resources\\app_icon.ico" - - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) -#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD -#else -#define VERSION_AS_NUMBER 1,0,0,0 -#endif - -#if defined(FLUTTER_VERSION) -#define VERSION_AS_STRING FLUTTER_VERSION -#else -#define VERSION_AS_STRING "1.0.0" -#endif - -VS_VERSION_INFO VERSIONINFO - FILEVERSION VERSION_AS_NUMBER - PRODUCTVERSION VERSION_AS_NUMBER - FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -#ifdef _DEBUG - FILEFLAGS VS_FF_DEBUG -#else - FILEFLAGS 0x0L -#endif - FILEOS VOS__WINDOWS32 - FILETYPE VFT_APP - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904e4" - BEGIN - VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "yappy" "\0" - VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "yappy" "\0" - VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "yappy.exe" "\0" - VALUE "ProductName", "yappy" "\0" - VALUE "ProductVersion", VERSION_AS_STRING "\0" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1252 - END -END - -#endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED diff --git a/team_b/yappy/windows/runner/flutter_window.cpp b/team_b/yappy/windows/runner/flutter_window.cpp deleted file mode 100644 index 955ee303..00000000 --- a/team_b/yappy/windows/runner/flutter_window.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include "flutter_window.h" - -#include - -#include "flutter/generated_plugin_registrant.h" - -FlutterWindow::FlutterWindow(const flutter::DartProject& project) - : project_(project) {} - -FlutterWindow::~FlutterWindow() {} - -bool FlutterWindow::OnCreate() { - if (!Win32Window::OnCreate()) { - return false; - } - - RECT frame = GetClientArea(); - - // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. - flutter_controller_ = std::make_unique( - frame.right - frame.left, frame.bottom - frame.top, project_); - // Ensure that basic setup of the controller was successful. - if (!flutter_controller_->engine() || !flutter_controller_->view()) { - return false; - } - RegisterPlugins(flutter_controller_->engine()); - SetChildContent(flutter_controller_->view()->GetNativeWindow()); - - flutter_controller_->engine()->SetNextFrameCallback([&]() { - this->Show(); - }); - - // Flutter can complete the first frame before the "show window" callback is - // registered. The following call ensures a frame is pending to ensure the - // window is shown. It is a no-op if the first frame hasn't completed yet. - flutter_controller_->ForceRedraw(); - - return true; -} - -void FlutterWindow::OnDestroy() { - if (flutter_controller_) { - flutter_controller_ = nullptr; - } - - Win32Window::OnDestroy(); -} - -LRESULT -FlutterWindow::MessageHandler(HWND hwnd, UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - // Give Flutter, including plugins, an opportunity to handle window messages. - if (flutter_controller_) { - std::optional result = - flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, - lparam); - if (result) { - return *result; - } - } - - switch (message) { - case WM_FONTCHANGE: - flutter_controller_->engine()->ReloadSystemFonts(); - break; - } - - return Win32Window::MessageHandler(hwnd, message, wparam, lparam); -} diff --git a/team_b/yappy/windows/runner/flutter_window.h b/team_b/yappy/windows/runner/flutter_window.h deleted file mode 100644 index 6da0652f..00000000 --- a/team_b/yappy/windows/runner/flutter_window.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef RUNNER_FLUTTER_WINDOW_H_ -#define RUNNER_FLUTTER_WINDOW_H_ - -#include -#include - -#include - -#include "win32_window.h" - -// A window that does nothing but host a Flutter view. -class FlutterWindow : public Win32Window { - public: - // Creates a new FlutterWindow hosting a Flutter view running |project|. - explicit FlutterWindow(const flutter::DartProject& project); - virtual ~FlutterWindow(); - - protected: - // Win32Window: - bool OnCreate() override; - void OnDestroy() override; - LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept override; - - private: - // The project to run. - flutter::DartProject project_; - - // The Flutter instance hosted by this window. - std::unique_ptr flutter_controller_; -}; - -#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/team_b/yappy/windows/runner/main.cpp b/team_b/yappy/windows/runner/main.cpp deleted file mode 100644 index d7fd7ce2..00000000 --- a/team_b/yappy/windows/runner/main.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include -#include -#include - -#include "flutter_window.h" -#include "utils.h" - -int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { - // Attach to console when present (e.g., 'flutter run') or create a - // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { - CreateAndAttachConsole(); - } - - // Initialize COM, so that it is available for use in the library and/or - // plugins. - ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - - flutter::DartProject project(L"data"); - - std::vector command_line_arguments = - GetCommandLineArguments(); - - project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); - - FlutterWindow window(project); - Win32Window::Point origin(10, 10); - Win32Window::Size size(1280, 720); - if (!window.Create(L"yappy", origin, size)) { - return EXIT_FAILURE; - } - window.SetQuitOnClose(true); - - ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { - ::TranslateMessage(&msg); - ::DispatchMessage(&msg); - } - - ::CoUninitialize(); - return EXIT_SUCCESS; -} diff --git a/team_b/yappy/windows/runner/resource.h b/team_b/yappy/windows/runner/resource.h deleted file mode 100644 index 66a65d1e..00000000 --- a/team_b/yappy/windows/runner/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by Runner.rc -// -#define IDI_APP_ICON 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/team_b/yappy/windows/runner/resources/app_icon.ico b/team_b/yappy/windows/runner/resources/app_icon.ico deleted file mode 100644 index c04e20ca..00000000 Binary files a/team_b/yappy/windows/runner/resources/app_icon.ico and /dev/null differ diff --git a/team_b/yappy/windows/runner/runner.exe.manifest b/team_b/yappy/windows/runner/runner.exe.manifest deleted file mode 100644 index 153653e8..00000000 --- a/team_b/yappy/windows/runner/runner.exe.manifest +++ /dev/null @@ -1,14 +0,0 @@ - - - - - PerMonitorV2 - - - - - - - - - diff --git a/team_b/yappy/windows/runner/utils.cpp b/team_b/yappy/windows/runner/utils.cpp deleted file mode 100644 index 3a0b4651..00000000 --- a/team_b/yappy/windows/runner/utils.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "utils.h" - -#include -#include -#include -#include - -#include - -void CreateAndAttachConsole() { - if (::AllocConsole()) { - FILE *unused; - if (freopen_s(&unused, "CONOUT$", "w", stdout)) { - _dup2(_fileno(stdout), 1); - } - if (freopen_s(&unused, "CONOUT$", "w", stderr)) { - _dup2(_fileno(stdout), 2); - } - std::ios::sync_with_stdio(); - FlutterDesktopResyncOutputStreams(); - } -} - -std::vector GetCommandLineArguments() { - // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. - int argc; - wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); - if (argv == nullptr) { - return std::vector(); - } - - std::vector command_line_arguments; - - // Skip the first argument as it's the binary name. - for (int i = 1; i < argc; i++) { - command_line_arguments.push_back(Utf8FromUtf16(argv[i])); - } - - ::LocalFree(argv); - - return command_line_arguments; -} - -std::string Utf8FromUtf16(const wchar_t* utf16_string) { - if (utf16_string == nullptr) { - return std::string(); - } - unsigned int target_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr) - -1; // remove the trailing null character - int input_length = (int)wcslen(utf16_string); - std::string utf8_string; - if (target_length == 0 || target_length > utf8_string.max_size()) { - return utf8_string; - } - utf8_string.resize(target_length); - int converted_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - input_length, utf8_string.data(), target_length, nullptr, nullptr); - if (converted_length == 0) { - return std::string(); - } - return utf8_string; -} diff --git a/team_b/yappy/windows/runner/utils.h b/team_b/yappy/windows/runner/utils.h deleted file mode 100644 index 3879d547..00000000 --- a/team_b/yappy/windows/runner/utils.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef RUNNER_UTILS_H_ -#define RUNNER_UTILS_H_ - -#include -#include - -// Creates a console for the process, and redirects stdout and stderr to -// it for both the runner and the Flutter library. -void CreateAndAttachConsole(); - -// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string -// encoded in UTF-8. Returns an empty std::string on failure. -std::string Utf8FromUtf16(const wchar_t* utf16_string); - -// Gets the command line arguments passed in as a std::vector, -// encoded in UTF-8. Returns an empty std::vector on failure. -std::vector GetCommandLineArguments(); - -#endif // RUNNER_UTILS_H_ diff --git a/team_b/yappy/windows/runner/win32_window.cpp b/team_b/yappy/windows/runner/win32_window.cpp deleted file mode 100644 index 60608d0f..00000000 --- a/team_b/yappy/windows/runner/win32_window.cpp +++ /dev/null @@ -1,288 +0,0 @@ -#include "win32_window.h" - -#include -#include - -#include "resource.h" - -namespace { - -/// Window attribute that enables dark mode window decorations. -/// -/// Redefined in case the developer's machine has a Windows SDK older than -/// version 10.0.22000.0. -/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute -#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE -#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 -#endif - -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; - -/// Registry key for app theme preference. -/// -/// A value of 0 indicates apps should use dark mode. A non-zero or missing -/// value indicates apps should use light mode. -constexpr const wchar_t kGetPreferredBrightnessRegKey[] = - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; -constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; - -// The number of Win32Window objects that currently exist. -static int g_active_window_count = 0; - -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); - -// Scale helper to convert logical scaler values to physical using passed in -// scale factor -int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} - -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; - } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); - } - FreeLibrary(user32_module); -} - -} // namespace - -// Manages the Win32Window's window class registration. -class WindowClassRegistrar { - public: - ~WindowClassRegistrar() = default; - - // Returns the singleton registrar instance. - static WindowClassRegistrar* GetInstance() { - if (!instance_) { - instance_ = new WindowClassRegistrar(); - } - return instance_; - } - - // Returns the name of the window class, registering the class if it hasn't - // previously been registered. - const wchar_t* GetWindowClass(); - - // Unregisters the window class. Should only be called if there are no - // instances of the window. - void UnregisterWindowClass(); - - private: - WindowClassRegistrar() = default; - - static WindowClassRegistrar* instance_; - - bool class_registered_ = false; -}; - -WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; - -const wchar_t* WindowClassRegistrar::GetWindowClass() { - if (!class_registered_) { - WNDCLASS window_class{}; - window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.lpszClassName = kWindowClassName; - window_class.style = CS_HREDRAW | CS_VREDRAW; - window_class.cbClsExtra = 0; - window_class.cbWndExtra = 0; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); - window_class.hbrBackground = 0; - window_class.lpszMenuName = nullptr; - window_class.lpfnWndProc = Win32Window::WndProc; - RegisterClass(&window_class); - class_registered_ = true; - } - return kWindowClassName; -} - -void WindowClassRegistrar::UnregisterWindowClass() { - UnregisterClass(kWindowClassName, nullptr); - class_registered_ = false; -} - -Win32Window::Win32Window() { - ++g_active_window_count; -} - -Win32Window::~Win32Window() { - --g_active_window_count; - Destroy(); -} - -bool Win32Window::Create(const std::wstring& title, - const Point& origin, - const Size& size) { - Destroy(); - - const wchar_t* window_class = - WindowClassRegistrar::GetInstance()->GetWindowClass(); - - const POINT target_point = {static_cast(origin.x), - static_cast(origin.y)}; - HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); - UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); - double scale_factor = dpi / 96.0; - - HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW, - Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), - Scale(size.width, scale_factor), Scale(size.height, scale_factor), - nullptr, nullptr, GetModuleHandle(nullptr), this); - - if (!window) { - return false; - } - - UpdateTheme(window); - - return OnCreate(); -} - -bool Win32Window::Show() { - return ShowWindow(window_handle_, SW_SHOWNORMAL); -} - -// static -LRESULT CALLBACK Win32Window::WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); - SetWindowLongPtr(window, GWLP_USERDATA, - reinterpret_cast(window_struct->lpCreateParams)); - - auto that = static_cast(window_struct->lpCreateParams); - EnableFullDpiSupportIfAvailable(window); - that->window_handle_ = window; - } else if (Win32Window* that = GetThisFromHandle(window)) { - return that->MessageHandler(window, message, wparam, lparam); - } - - return DefWindowProc(window, message, wparam, lparam); -} - -LRESULT -Win32Window::MessageHandler(HWND hwnd, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: - window_handle_ = nullptr; - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; - - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - - return 0; - } - case WM_SIZE: { - RECT rect = GetClientArea(); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; - } - - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - - case WM_DWMCOLORIZATIONCOLORCHANGED: - UpdateTheme(hwnd); - return 0; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); -} - -void Win32Window::Destroy() { - OnDestroy(); - - if (window_handle_) { - DestroyWindow(window_handle_); - window_handle_ = nullptr; - } - if (g_active_window_count == 0) { - WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); - } -} - -Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { - return reinterpret_cast( - GetWindowLongPtr(window, GWLP_USERDATA)); -} - -void Win32Window::SetChildContent(HWND content) { - child_content_ = content; - SetParent(content, window_handle_); - RECT frame = GetClientArea(); - - MoveWindow(content, frame.left, frame.top, frame.right - frame.left, - frame.bottom - frame.top, true); - - SetFocus(child_content_); -} - -RECT Win32Window::GetClientArea() { - RECT frame; - GetClientRect(window_handle_, &frame); - return frame; -} - -HWND Win32Window::GetHandle() { - return window_handle_; -} - -void Win32Window::SetQuitOnClose(bool quit_on_close) { - quit_on_close_ = quit_on_close; -} - -bool Win32Window::OnCreate() { - // No-op; provided for subclasses. - return true; -} - -void Win32Window::OnDestroy() { - // No-op; provided for subclasses. -} - -void Win32Window::UpdateTheme(HWND const window) { - DWORD light_mode; - DWORD light_mode_size = sizeof(light_mode); - LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, - kGetPreferredBrightnessRegValue, - RRF_RT_REG_DWORD, nullptr, &light_mode, - &light_mode_size); - - if (result == ERROR_SUCCESS) { - BOOL enable_dark_mode = light_mode == 0; - DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, - &enable_dark_mode, sizeof(enable_dark_mode)); - } -} diff --git a/team_b/yappy/windows/runner/win32_window.h b/team_b/yappy/windows/runner/win32_window.h deleted file mode 100644 index e901dde6..00000000 --- a/team_b/yappy/windows/runner/win32_window.h +++ /dev/null @@ -1,102 +0,0 @@ -#ifndef RUNNER_WIN32_WINDOW_H_ -#define RUNNER_WIN32_WINDOW_H_ - -#include - -#include -#include -#include - -// A class abstraction for a high DPI-aware Win32 Window. Intended to be -// inherited from by classes that wish to specialize with custom -// rendering and input handling -class Win32Window { - public: - struct Point { - unsigned int x; - unsigned int y; - Point(unsigned int x, unsigned int y) : x(x), y(y) {} - }; - - struct Size { - unsigned int width; - unsigned int height; - Size(unsigned int width, unsigned int height) - : width(width), height(height) {} - }; - - Win32Window(); - virtual ~Win32Window(); - - // Creates a win32 window with |title| that is positioned and sized using - // |origin| and |size|. New windows are created on the default monitor. Window - // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size this function will scale the inputted width and height as - // as appropriate for the default monitor. The window is invisible until - // |Show| is called. Returns true if the window was created successfully. - bool Create(const std::wstring& title, const Point& origin, const Size& size); - - // Show the current window. Returns true if the window was successfully shown. - bool Show(); - - // Release OS resources associated with window. - void Destroy(); - - // Inserts |content| into the window tree. - void SetChildContent(HWND content); - - // Returns the backing Window handle to enable clients to set icon and other - // window properties. Returns nullptr if the window has been destroyed. - HWND GetHandle(); - - // If true, closing this window will quit the application. - void SetQuitOnClose(bool quit_on_close); - - // Return a RECT representing the bounds of the current client area. - RECT GetClientArea(); - - protected: - // Processes and route salient window messages for mouse handling, - // size change and DPI. Delegates handling of these to member overloads that - // inheriting classes can handle. - virtual LRESULT MessageHandler(HWND window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Called when CreateAndShow is called, allowing subclass window-related - // setup. Subclasses should return false if setup fails. - virtual bool OnCreate(); - - // Called when Destroy is called. - virtual void OnDestroy(); - - private: - friend class WindowClassRegistrar; - - // OS callback called by message pump. Handles the WM_NCCREATE message which - // is passed when the non-client area is being created and enables automatic - // non-client DPI scaling so that the non-client area automatically - // responds to changes in DPI. All other messages are handled by - // MessageHandler. - static LRESULT CALLBACK WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Retrieves a class instance pointer for |window| - static Win32Window* GetThisFromHandle(HWND const window) noexcept; - - // Update the window frame's theme to match the system theme. - static void UpdateTheme(HWND const window); - - bool quit_on_close_ = false; - - // window handle for top level window. - HWND window_handle_ = nullptr; - - // window handle for hosted content. - HWND child_content_ = nullptr; -}; - -#endif // RUNNER_WIN32_WINDOW_H_