diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 3c9f486ff..bdb7a3ef3 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5291,5 +5291,9 @@ "allCefrLevels": "All CEFR levels", "chatListTooltip": "Here you’ll find your direct messages! Click on any user’s avatar and “start conversation” to send a DM.", "directMessageBotTitle": "Direct message Pangea Bot", + "feedbackTitle": "Activity Feedback", + "feedbackDesc": "How should the activity be improved? If you can provide some details, we’ll make the change!", + "feedbackHint": "Your feedback", + "feedbackButton": "Submit feedback", "directMessageBotDesc": "Talking to humans is more fun but... AI is always ready!" } diff --git a/lib/pangea/activity_feedback/activity_feedback_repo.dart b/lib/pangea/activity_feedback/activity_feedback_repo.dart new file mode 100644 index 000000000..21d8a8522 --- /dev/null +++ b/lib/pangea/activity_feedback/activity_feedback_repo.dart @@ -0,0 +1,40 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; + +import 'package:fluffychat/pangea/activity_feedback/activity_feedback_request.dart'; +import 'package:fluffychat/pangea/activity_feedback/activity_feedback_response.dart'; +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/common/network/requests.dart'; +import 'package:fluffychat/pangea/common/network/urls.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class ActivityFeedbackRepo { + /// Submit activity feedback for processing + /// + /// This method sends user feedback about an activity to the server + /// for evaluation and potential activity edits. The feedback is processed + /// in the background and the response indicates whether edits will be made. + static Future submitFeedback( + ActivityFeedbackRequest request, + ) async { + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: MatrixState.pangeaController.userController.accessToken, + ); + + final Response res = await req.post( + url: PApiUrls.activityFeedback, + body: request.toJson(), + ); + + if (res.statusCode != 200) { + throw Exception( + 'Failed to submit activity feedback: ${res.statusCode} ${res.body}', + ); + } + + final decodedBody = jsonDecode(utf8.decode(res.bodyBytes)); + return ActivityFeedbackResponse.fromJson(decodedBody); + } +} diff --git a/lib/pangea/activity_feedback/activity_feedback_request.dart b/lib/pangea/activity_feedback/activity_feedback_request.dart new file mode 100644 index 000000000..b7ca806b7 --- /dev/null +++ b/lib/pangea/activity_feedback/activity_feedback_request.dart @@ -0,0 +1,44 @@ +class ActivityFeedbackRequest { + final String activityId; + final String feedbackText; + final String userId; + final String userL1; + final String userL2; + + ActivityFeedbackRequest({ + required this.activityId, + required this.feedbackText, + required this.userId, + required this.userL1, + required this.userL2, + }); + + Map toJson() { + return { + 'activity_id': activityId, + 'feedback_text': feedbackText, + 'user_id': userId, + 'user_l1': userL1, + 'user_l2': userL2, + }; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ActivityFeedbackRequest && + runtimeType == other.runtimeType && + activityId == other.activityId && + feedbackText == other.feedbackText && + userId == other.userId && + userL1 == other.userL1 && + userL2 == other.userL2; + + @override + int get hashCode => + activityId.hashCode ^ + feedbackText.hashCode ^ + userId.hashCode ^ + userL1.hashCode ^ + userL2.hashCode; +} diff --git a/lib/pangea/activity_feedback/activity_feedback_response.dart b/lib/pangea/activity_feedback/activity_feedback_response.dart new file mode 100644 index 000000000..50c9bdd09 --- /dev/null +++ b/lib/pangea/activity_feedback/activity_feedback_response.dart @@ -0,0 +1,45 @@ +import 'package:fluffychat/pangea/events/models/content_feedback.dart'; + +class ActivityFeedbackResponse implements JsonSerializable { + final bool shouldMakeEdits; + final String? cleanedEditPrompt; + final String userFriendlyResponse; + + ActivityFeedbackResponse({ + required this.shouldMakeEdits, + this.cleanedEditPrompt, + required this.userFriendlyResponse, + }); + + factory ActivityFeedbackResponse.fromJson(Map json) { + return ActivityFeedbackResponse( + shouldMakeEdits: json['should_make_edits'] as bool, + cleanedEditPrompt: json['cleaned_edit_prompt'] as String?, + userFriendlyResponse: json['user_friendly_response'] as String, + ); + } + + @override + Map toJson() { + return { + 'should_make_edits': shouldMakeEdits, + 'cleaned_edit_prompt': cleanedEditPrompt, + 'user_friendly_response': userFriendlyResponse, + }; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ActivityFeedbackResponse && + runtimeType == other.runtimeType && + shouldMakeEdits == other.shouldMakeEdits && + cleanedEditPrompt == other.cleanedEditPrompt && + userFriendlyResponse == other.userFriendlyResponse; + + @override + int get hashCode => + shouldMakeEdits.hashCode ^ + cleanedEditPrompt.hashCode ^ + userFriendlyResponse.hashCode; +} diff --git a/lib/pangea/activity_sessions/activity_session_start/activity_feedback_request_dialog.dart b/lib/pangea/activity_sessions/activity_session_start/activity_feedback_request_dialog.dart new file mode 100644 index 000000000..adbed6422 --- /dev/null +++ b/lib/pangea/activity_sessions/activity_session_start/activity_feedback_request_dialog.dart @@ -0,0 +1,121 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; + +class ActivityFeedbackRequestDialog extends StatefulWidget { + const ActivityFeedbackRequestDialog({super.key}); + + @override + State createState() => + ActivityFeedbackRequestDialogState(); +} + +class ActivityFeedbackRequestDialogState + extends State { + final TextEditingController _feedbackController = TextEditingController(); + + @override + void initState() { + super.initState(); + _feedbackController.addListener(() { + setState(() {}); + }); + } + + @override + void dispose() { + _feedbackController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 2.5, sigmaY: 2.5), + child: Dialog( + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), + ), + child: SizedBox( + width: 325.0, + child: Column( + spacing: 20.0, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + Expanded( + child: Text( + L10n.of(context).feedbackTitle, + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + ), + ), + const SizedBox( + width: 40.0, + height: 40.0, + child: Center( + child: Icon(Icons.flag_outlined), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20.0, + ), + child: Column( + spacing: 20.0, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + L10n.of(context).feedbackDesc, + textAlign: TextAlign.center, + ), + SizedBox( + child: TextField( + controller: _feedbackController, + decoration: InputDecoration( + hintText: L10n.of(context).feedbackHint, + ), + keyboardType: TextInputType.multiline, + minLines: 1, + maxLines: 5, + ), + ), + ElevatedButton( + onPressed: _feedbackController.text.isNotEmpty + ? () { + Navigator.of(context).pop( + _feedbackController.text, + ); + } + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(L10n.of(context).feedbackButton), + ], + ), + ), + const SizedBox.shrink(), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pangea/activity_sessions/activity_session_start/activity_feedback_response_dialog.dart b/lib/pangea/activity_sessions/activity_session_start/activity_feedback_response_dialog.dart new file mode 100644 index 000000000..e1962e023 --- /dev/null +++ b/lib/pangea/activity_sessions/activity_session_start/activity_feedback_response_dialog.dart @@ -0,0 +1,78 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart'; + +class ActivityFeedbackResponseDialog extends StatelessWidget { + final String feedback; + const ActivityFeedbackResponseDialog({super.key, required this.feedback}); + + @override + Widget build(BuildContext context) { + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 2.5, sigmaY: 2.5), + child: Dialog( + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), + ), + child: SizedBox( + width: 325.0, + child: Column( + spacing: 20.0, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + Expanded( + child: Text( + L10n.of(context).feedbackTitle, + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + ), + ), + const SizedBox( + width: 40.0, + height: 40.0, + child: Center( + child: Icon(Icons.flag_outlined), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20.0, + ), + child: Column( + spacing: 20.0, + mainAxisSize: MainAxisSize.min, + children: [ + const BotFace( + width: 60.0, + expression: BotExpression.idle, + ), + Text( + feedback, + textAlign: TextAlign.center, + ), + const SizedBox.shrink(), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart b/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart index fc4c9d155..4c4674a5e 100644 --- a/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart +++ b/lib/pangea/activity_sessions/activity_session_start/activity_sessions_start_view.dart @@ -4,6 +4,10 @@ import 'package:go_router/go_router.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/activity_feedback/activity_feedback_repo.dart'; +import 'package:fluffychat/pangea/activity_feedback/activity_feedback_request.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_feedback_request_dialog.dart'; +import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_feedback_response_dialog.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activity_session_start_page.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_summary_widget.dart'; import 'package:fluffychat/pangea/common/widgets/error_indicator.dart'; @@ -53,6 +57,51 @@ class ActivitySessionStartView extends StatelessWidget { ), ), ), + actions: [ + IconButton( + icon: const Icon(Icons.flag_outlined), + onPressed: () async { + final feedback = await showDialog( + context: context, + builder: (context) { + return const ActivityFeedbackRequestDialog(); + }, + ); + + if (feedback == null || feedback.isEmpty) { + return; + } + + final resp = await showFutureLoadingDialog( + context: context, + future: () => ActivityFeedbackRepo.submitFeedback( + ActivityFeedbackRequest( + activityId: controller.widget.activityId, + feedbackText: feedback, + userId: Matrix.of(context).client.userID!, + userL1: MatrixState.pangeaController.languageController + .activeL1Code()!, + userL2: MatrixState.pangeaController.languageController + .activeL2Code()!, + ), + ), + ); + + if (resp.isError) { + return; + } + + await showDialog( + context: context, + builder: (context) { + return ActivityFeedbackResponseDialog( + feedback: resp.result!.userFriendlyResponse, + ); + }, + ); + }, + ), + ], ), body: SafeArea( child: controller.loading diff --git a/lib/pangea/common/network/urls.dart b/lib/pangea/common/network/urls.dart index 5e436ee15..43bb0e147 100644 --- a/lib/pangea/common/network/urls.dart +++ b/lib/pangea/common/network/urls.dart @@ -66,6 +66,9 @@ class PApiUrls { static String activitySummary = "${PApiUrls._choreoEndpoint}/activity_summary"; + static String activityFeedback = + "${PApiUrls._choreoEndpoint}/activity_plan/feedback"; + static String morphFeaturesAndTags = "${PApiUrls._choreoEndpoint}/morphs"; static String constructSummary = "${PApiUrls._choreoEndpoint}/construct_summary";