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:
wcjord 2025-09-26 14:30:41 -04:00 committed by GitHub
parent 700f53c68d
commit 951d8ef626
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 384 additions and 0 deletions

View file

@ -5291,5 +5291,9 @@
"allCefrLevels": "All CEFR levels",
"chatListTooltip": "Here youll find your direct messages! Click on any users 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, well make the change!",
"feedbackHint": "Your feedback",
"feedbackButton": "Submit feedback",
"directMessageBotDesc": "Talking to humans is more fun but... AI is always ready!"
}

View 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);
}
}

View 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;
}

View 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;
}

View file

@ -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(),
],
),
),
],
),
),
),
);
}
}

View file

@ -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(),
],
),
),
],
),
),
),
);
}
}

View file

@ -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

View file

@ -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";