4142-allow-giving-of-activity-feedback (#4144)
* feat: activity feedback repo * add UI for giving activity feedback --------- Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com> Co-authored-by: ggurdin <ggurdin@gmail.com>
This commit is contained in:
parent
700f53c68d
commit
951d8ef626
8 changed files with 384 additions and 0 deletions
|
|
@ -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!"
|
||||
}
|
||||
|
|
|
|||
40
lib/pangea/activity_feedback/activity_feedback_repo.dart
Normal file
40
lib/pangea/activity_feedback/activity_feedback_repo.dart
Normal file
|
|
@ -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<ActivityFeedbackResponse> 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);
|
||||
}
|
||||
}
|
||||
44
lib/pangea/activity_feedback/activity_feedback_request.dart
Normal file
44
lib/pangea/activity_feedback/activity_feedback_request.dart
Normal file
|
|
@ -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<String, dynamic> 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;
|
||||
}
|
||||
45
lib/pangea/activity_feedback/activity_feedback_response.dart
Normal file
45
lib/pangea/activity_feedback/activity_feedback_response.dart
Normal file
|
|
@ -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<String, dynamic> 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<String, dynamic> 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;
|
||||
}
|
||||
|
|
@ -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<ActivityFeedbackRequestDialog> createState() =>
|
||||
ActivityFeedbackRequestDialogState();
|
||||
}
|
||||
|
||||
class ActivityFeedbackRequestDialogState
|
||||
extends State<ActivityFeedbackRequestDialog> {
|
||||
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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String?>(
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue