Toolbar practice (#707)

* remove print statement

* ending animation, savoring joy, properly adding xp in session

* forgot to switch env again...

* increment version number

* about to move toolbar buttons up to level of overlay controller

* added ability to give feedback and get new activity
pull/1398/head
wcjord 1 year ago committed by GitHub
parent 371d4f06d4
commit b8edf595ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -4121,7 +4121,7 @@
"suggestToSpaceDesc": "Suggested sub spaces will appear in their main space's chat list",
"practice": "Practice",
"noLanguagesSet": "No languages set",
"noActivitiesFound": "You're all practiced for now! Come back later for more.",
"noActivitiesFound": "That's enough on this for now! Come back later for more.",
"hintTitle": "Hint:",
"speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores",
"previous": "Previous",
@ -4229,5 +4229,8 @@
"grammar": "Grammar",
"contactHasBeenInvitedToTheChat": "Contact has been invited to the chat",
"inviteChat": "📨 Invite chat",
"chatName": "Chat name"
"chatName": "Chat name",
"reportContentIssueTitle": "Report content issue",
"feedback": "Your feedback (optional)",
"reportContentIssueDescription": "Sorry! AI can make personalized experiences but also may have issues. Please provide any feedback you have and we'll generate a new activity."
}

@ -4737,5 +4737,6 @@
}
},
"commandHint_googly": "Enviar unos ojos saltones",
"@commandHint_googly": {}
"@commandHint_googly": {},
"reportContentIssue": "Problema de contenido"
}

@ -4,7 +4,7 @@ import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/events/video_player.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar_selection_area.dart';
import 'package:fluffychat/pangea/widgets/chat/overlay_message_text.dart';
import 'package:fluffychat/pangea/widgets/igc/pangea_rich_text.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';

@ -110,7 +110,8 @@ class ExistingActivityMetaData {
factory ExistingActivityMetaData.fromJson(Map<String, dynamic> json) {
return ExistingActivityMetaData(
activityEventId: json['activity_event_id'] as String,
tgtConstructs: (json['tgt_constructs'] as List)
tgtConstructs: ((json['tgt_constructs'] ?? json['target_constructs'])
as List)
.map((e) => ConstructIdentifier.fromJson(e as Map<String, dynamic>))
.toList(),
activityType: ActivityTypeEnum.values.firstWhere(
@ -124,18 +125,47 @@ class ExistingActivityMetaData {
Map<String, dynamic> toJson() {
return {
'activity_event_id': activityEventId,
'tgt_constructs': tgtConstructs.map((e) => e.toJson()).toList(),
'target_constructs': tgtConstructs.map((e) => e.toJson()).toList(),
'activity_type': activityType.string,
};
}
}
// includes feedback text and the bad activity model
class ActivityQualityFeedback {
final String feedbackText;
final PracticeActivityModel badActivity;
ActivityQualityFeedback({
required this.feedbackText,
required this.badActivity,
});
factory ActivityQualityFeedback.fromJson(Map<String, dynamic> json) {
return ActivityQualityFeedback(
feedbackText: json['feedback_text'] as String,
badActivity: PracticeActivityModel.fromJson(
json['bad_activity'] as Map<String, dynamic>,
),
);
}
Map<String, dynamic> toJson() {
return {
'feedback_text': feedbackText,
'bad_activity': badActivity.toJson(),
};
}
}
class MessageActivityRequest {
final String userL1;
final String userL2;
final String messageText;
final ActivityQualityFeedback? activityQualityFeedback;
/// tokens with their associated constructs and xp
final List<TokenWithXP> tokensWithXP;
@ -151,6 +181,7 @@ class MessageActivityRequest {
required this.tokensWithXP,
required this.messageId,
required this.existingActivities,
required this.activityQualityFeedback,
});
factory MessageActivityRequest.fromJson(Map<String, dynamic> json) {
@ -167,6 +198,11 @@ class MessageActivityRequest {
(e) => ExistingActivityMetaData.fromJson(e as Map<String, dynamic>),
)
.toList(),
activityQualityFeedback: json['activity_quality_feedback'] != null
? ActivityQualityFeedback.fromJson(
json['activity_quality_feedback'] as Map<String, dynamic>,
)
: null,
);
}
@ -178,6 +214,7 @@ class MessageActivityRequest {
'tokens_with_xp': tokensWithXP.map((e) => e.toJson()).toList(),
'message_id': messageId,
'existing_activities': existingActivities.map((e) => e.toJson()).toList(),
'activity_quality_feedback': activityQualityFeedback?.toJson(),
};
}

@ -270,7 +270,8 @@ class PracticeActivityModel {
factory PracticeActivityModel.fromJson(Map<String, dynamic> json) {
return PracticeActivityModel(
tgtConstructs: (json['tgt_constructs'] as List)
tgtConstructs: ((json['tgt_constructs'] ?? json['target_constructs'])
as List)
.map((e) => ConstructIdentifier.fromJson(e as Map<String, dynamic>))
.toList(),
langCode: json['lang_code'] as String,
@ -278,7 +279,9 @@ class PracticeActivityModel {
activityType: json['activity_type'] == "multipleChoice"
? ActivityTypeEnum.multipleChoice
: ActivityTypeEnum.values.firstWhere(
(e) => e.string == json['activity_type'],
(e) =>
e.string == json['activity_type'] as String ||
e.string.split('.').last == json['activity_type'] as String,
),
multipleChoice: json['multiple_choice'] != null
? MultipleChoice.fromJson(
@ -301,12 +304,13 @@ class PracticeActivityModel {
RelevantSpanDisplayDetails? get relevantSpanDisplayDetails =>
multipleChoice?.spanDisplayDetails;
Map<String, dynamic> toJson() {
return {
'tgt_constructs': tgtConstructs.map((e) => e.toJson()).toList(),
'target_constructs': tgtConstructs.map((e) => e.toJson()).toList(),
'lang_code': langCode,
'msg_id': msgId,
'activity_type': activityType.toString().split('.').last,
'activity_type': activityType.string,
'multiple_choice': multipleChoice?.toJson(),
'listening': listening?.toJson(),
'speaking': speaking?.toJson(),

@ -76,6 +76,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(8),
child: _isLoading
? const ToolbarContentLoadingIndicator()
: localAudioEvent != null || audioFile != null

@ -57,6 +57,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
int activitiesLeftToComplete = neededActivities;
PangeaMessageEvent get pangeaMessageEvent => widget._pangeaMessageEvent;
@override
void initState() {
super.initState();
@ -301,13 +303,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
? MainAxisAlignment.end
: MainAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(
left: widget._pangeaMessageEvent.ownMessage
? 0
: Avatar.defaultSize + 16,
right: widget._pangeaMessageEvent.ownMessage ? 8 : 0,
),
MessagePadding(
pangeaMessageEvent: pangeaMessageEvent,
child: MessageToolbar(
pangeaMessageEvent: widget._pangeaMessageEvent,
overLayController: this,
@ -330,6 +327,13 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
nextEvent: widget._nextEvent,
previousEvent: widget._prevEvent,
),
// TODO for @ggurdin - move reactions and toolbar here
// MessageReactions(widget._event, widget.chatController.timeline!),
// const SizedBox(height: 6),
// MessagePadding(
// pangeaMessageEvent: pangeaMessageEvent,
// child: ToolbarButtons(overlayController: this, width: 250),
// ),
],
),
),
@ -396,3 +400,25 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
);
}
}
class MessagePadding extends StatelessWidget {
const MessagePadding({
super.key,
required this.child,
required this.pangeaMessageEvent,
});
final Widget child;
final PangeaMessageEvent pangeaMessageEvent;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
left: pangeaMessageEvent.ownMessage ? 0 : Avatar.defaultSize + 16,
right: pangeaMessageEvent.ownMessage ? 8 : 0,
),
child: child,
);
}
}

@ -1,16 +1,14 @@
import 'dart:developer';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart';
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar_buttons.dart';
import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart';
import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart';
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
@ -18,7 +16,6 @@ import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_ca
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
class MessageToolbar extends StatefulWidget {
final PangeaMessageEvent pangeaMessageEvent;
@ -35,33 +32,9 @@ class MessageToolbar extends StatefulWidget {
}
class MessageToolbarState extends State<MessageToolbar> {
bool updatingMode = false;
@override
void initState() {
super.initState();
// why can't this just be initstate or the build mode?
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
// //determine the starting mode
// // if (widget.pangeaMessageEvent.isAudioMessage) {
// // updateMode(MessageMode.speechToText);
// // return;
// // }
// // if (widget.initialMode != null) {
// // updateMode(widget.initialMode!);
// // } else {
// // MatrixState.pangeaController.userController.profile.userSettings
// // .autoPlayMessages
// // ? updateMode(MessageMode.textToSpeech)
// // : updateMode(MessageMode.translation);
// // }
// // });
// // just set mode based on messageSelectionOverlay mode which is now handling the state
// updateMode(widget.overLayController.toolbarMode);
// });
}
Widget get toolbarContent {
@ -141,27 +114,28 @@ class MessageToolbarState extends State<MessageToolbar> {
@override
Widget build(BuildContext context) {
debugPrint("building toolbar");
return Material(
key: MatrixState.pAnyState
.layerLinkAndKey('${widget.pangeaMessageEvent.eventId}-toolbar')
.key,
type: MaterialType.transparency,
child: Container(
child: Column(
children: [
Container(
constraints: const BoxConstraints(
maxHeight: AppConfig.toolbarMaxHeight,
maxWidth: 275,
minWidth: 275,
maxWidth: 350,
minWidth: 350,
),
padding: const EdgeInsets.all(20),
padding: const EdgeInsets.all(0),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
border: Border.all(
width: 2,
color: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.primary.withOpacity(0.5),
),
borderRadius: const BorderRadius.all(
Radius.circular(25),
Radius.circular(AppConfig.borderRadius),
),
),
child: Column(
@ -175,174 +149,15 @@ class MessageToolbarState extends State<MessageToolbar> {
),
),
),
ToolbarButtons(messageToolbarController: this, width: 250),
],
),
),
);
}
}
class ToolbarSelectionArea extends StatelessWidget {
final ChatController controller;
final PangeaMessageEvent? pangeaMessageEvent;
final bool isOverlay;
final Widget child;
final Event? nextEvent;
final Event? prevEvent;
const ToolbarSelectionArea({
required this.controller,
this.pangeaMessageEvent,
this.isOverlay = false,
required this.child,
this.nextEvent,
this.prevEvent,
super.key,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (pangeaMessageEvent != null && !isOverlay) {
controller.showToolbar(
pangeaMessageEvent!,
nextEvent: nextEvent,
prevEvent: prevEvent,
);
}
},
onLongPress: () {
if (pangeaMessageEvent != null && !isOverlay) {
controller.showToolbar(
pangeaMessageEvent!,
nextEvent: nextEvent,
prevEvent: prevEvent,
);
}
},
child: child,
);
}
}
class ToolbarButtons extends StatefulWidget {
final MessageToolbarState messageToolbarController;
final double width;
const ToolbarButtons({
required this.messageToolbarController,
required this.width,
super.key,
});
@override
ToolbarButtonsState createState() => ToolbarButtonsState();
}
class ToolbarButtonsState extends State<ToolbarButtons> {
PangeaMessageEvent get pangeaMessageEvent =>
widget.messageToolbarController.widget.pangeaMessageEvent;
List<MessageMode> get modes => MessageMode.values
.where((mode) => mode.isValidMode(pangeaMessageEvent.event))
.toList();
static const double iconWidth = 36.0;
MessageOverlayController get overlayController =>
widget.messageToolbarController.widget.overLayController;
// @ggurdin - maybe this can be stateless now?
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final double barWidth = widget.width - iconWidth;
if (widget
.messageToolbarController.widget.pangeaMessageEvent.isAudioMessage) {
return const SizedBox();
}
return SizedBox(
width: widget.width,
child: Stack(
alignment: Alignment.center,
children: [
Stack(
children: [
Container(
width: widget.width,
height: 12,
decoration: BoxDecoration(
color: MessageModeExtension.barAndLockedButtonColor(context),
),
margin: const EdgeInsets.symmetric(horizontal: iconWidth / 2),
),
AnimatedContainer(
duration: FluffyThemes.animationDuration,
height: 12,
width: overlayController.isPracticeComplete
? barWidth
: min(
barWidth,
(barWidth / 3) *
pangeaMessageEvent.numberOfActivitiesCompleted,
),
color: AppConfig.success,
margin: const EdgeInsets.symmetric(horizontal: iconWidth / 2),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: modes
.mapIndexed(
(index, mode) => Tooltip(
message: mode.tooltip(context),
child: IconButton(
iconSize: 20,
icon: Icon(mode.icon),
color: mode ==
widget.messageToolbarController.widget
.overLayController.toolbarMode
? Colors.white
: null,
isSelected: mode ==
widget.messageToolbarController.widget
.overLayController.toolbarMode,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
mode.iconButtonColor(
context,
index,
widget.messageToolbarController.widget
.overLayController.toolbarMode,
pangeaMessageEvent.numberOfActivitiesCompleted,
widget.messageToolbarController.widget
.overLayController.isPracticeComplete,
),
),
const SizedBox(height: 6),
ToolbarButtons(
overlayController: widget.overLayController,
width: 250,
),
onPressed: mode.isUnlocked(
index,
pangeaMessageEvent.numberOfActivitiesCompleted,
overlayController.isPracticeComplete,
)
? () => widget
.messageToolbarController.widget.overLayController
.updateToolbarMode(mode)
: null,
),
),
)
.toList(),
),
const SizedBox(height: 6),
],
),
);

@ -0,0 +1,122 @@
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
import 'package:flutter/material.dart';
class ToolbarButtons extends StatefulWidget {
final MessageOverlayController overlayController;
final double width;
const ToolbarButtons({
required this.overlayController,
required this.width,
super.key,
});
@override
ToolbarButtonsState createState() => ToolbarButtonsState();
}
class ToolbarButtonsState extends State<ToolbarButtons> {
PangeaMessageEvent get pangeaMessageEvent =>
widget.overlayController.pangeaMessageEvent;
List<MessageMode> get modes => MessageMode.values
.where((mode) => mode.isValidMode(pangeaMessageEvent.event))
.toList();
static const double iconWidth = 36.0;
MessageOverlayController get overlayController => widget.overlayController;
// @ggurdin - maybe this can be stateless now?
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final double barWidth = widget.width - iconWidth;
if (widget.overlayController.pangeaMessageEvent.isAudioMessage) {
return const SizedBox();
}
return SizedBox(
width: widget.width,
child: Stack(
alignment: Alignment.center,
children: [
Stack(
children: [
Container(
width: widget.width,
height: 12,
decoration: BoxDecoration(
color: MessageModeExtension.barAndLockedButtonColor(context),
),
margin: const EdgeInsets.symmetric(horizontal: iconWidth / 2),
),
AnimatedContainer(
duration: FluffyThemes.animationDuration,
height: 12,
width: overlayController.isPracticeComplete
? barWidth
: min(
barWidth,
(barWidth / 3) *
pangeaMessageEvent.numberOfActivitiesCompleted,
),
color: AppConfig.success,
margin: const EdgeInsets.symmetric(horizontal: iconWidth / 2),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: modes
.mapIndexed(
(index, mode) => Tooltip(
message: mode.tooltip(context),
child: IconButton(
iconSize: 20,
icon: Icon(mode.icon),
color: mode == widget.overlayController.toolbarMode
? Colors.white
: null,
isSelected: mode == widget.overlayController.toolbarMode,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
mode.iconButtonColor(
context,
index,
widget.overlayController.toolbarMode,
pangeaMessageEvent.numberOfActivitiesCompleted,
widget.overlayController.isPracticeComplete,
),
),
),
onPressed: mode.isUnlocked(
index,
pangeaMessageEvent.numberOfActivitiesCompleted,
overlayController.isPracticeComplete,
)
? () =>
widget.overlayController.updateToolbarMode(mode)
: null,
),
),
)
.toList(),
),
],
),
);
}
}

@ -0,0 +1,48 @@
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
class ToolbarSelectionArea extends StatelessWidget {
final ChatController controller;
final PangeaMessageEvent? pangeaMessageEvent;
final bool isOverlay;
final Widget child;
final Event? nextEvent;
final Event? prevEvent;
const ToolbarSelectionArea({
required this.controller,
this.pangeaMessageEvent,
this.isOverlay = false,
required this.child,
this.nextEvent,
this.prevEvent,
super.key,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (pangeaMessageEvent != null && !isOverlay) {
controller.showToolbar(
pangeaMessageEvent!,
nextEvent: nextEvent,
prevEvent: prevEvent,
);
}
},
onLongPress: () {
if (pangeaMessageEvent != null && !isOverlay) {
controller.showToolbar(
pangeaMessageEvent!,
nextEvent: nextEvent,
prevEvent: prevEvent,
);
}
},
child: child,
);
}
}

@ -151,6 +151,7 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
}
return Container(
padding: const EdgeInsets.all(8),
child: _fetchingTranslation
? const ToolbarContentLoadingIndicator()
: Column(
@ -170,6 +171,7 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
body: InlineInstructions.l1Translation.body(context),
onClose: closeHint,
),
// if (widget.selection != null)
],
),
);

@ -8,7 +8,7 @@ import 'package:fluffychat/pangea/enum/instructions_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/representation_content_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar_selection_area.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

@ -174,7 +174,9 @@ class WordDataCardView extends StatelessWidget {
final ScrollController scrollController = ScrollController();
return Scrollbar(
return Container(
padding: const EdgeInsets.all(8),
child: Scrollbar(
thumbVisibility: true,
controller: scrollController,
child: SingleChildScrollView(
@ -226,6 +228,7 @@ class WordDataCardView extends StatelessWidget {
],
),
),
),
);
}
}
@ -405,12 +408,18 @@ class SelectToDefine extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
return Center(
child: Container(
height: 80,
width: 200,
padding: const EdgeInsets.all(8),
child: Center(
child: Text(
L10n.of(context)!.selectToDefine,
style: BotStyle.text(context),
),
),
),
);
}
}

@ -81,6 +81,7 @@ class GamifiedTextWidget extends StatelessWidget {
constraints: const BoxConstraints(
minHeight: 80,
),
padding: const EdgeInsets.all(8),
child: Text(
userMessage,
style: const TextStyle(

@ -6,7 +6,6 @@ import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
@ -16,6 +15,7 @@ import 'package:fluffychat/pangea/widgets/animations/gain_points.dart';
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/no_more_practice_card.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/target_tokens_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -44,6 +44,8 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
PracticeActivityRecordModel? currentCompletionRecord;
bool fetchingActivity = false;
// tracks the target tokens for the current message
// in a separate controller to manage the state
TargetTokensController targetTokensController = TargetTokensController();
List<PracticeActivityEvent> get practiceActivities =>
@ -108,7 +110,9 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
return incompleteActivities.firstOrNull;
}
Future<PracticeActivityEvent?> _fetchNewActivity() async {
Future<PracticeActivityEvent?> _fetchNewActivity([
ActivityQualityFeedback? activityFeedback,
]) async {
try {
debugPrint('Fetching new activity');
@ -143,6 +147,7 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
existingActivities: practiceActivities
.map((activity) => activity.activityRequestMetaData)
.toList(),
activityQualityFeedback: activityFeedback,
),
widget.pangeaMessageEvent,
);
@ -192,7 +197,7 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
/// Fetches a new activity if there are any left to complete.
/// Exits the practice flow if there are no more activities.
void onActivityFinish() async {
// try {
try {
if (currentCompletionRecord == null || currentActivity == null) {
debugger(when: kDebugMode);
return;
@ -234,20 +239,96 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
]);
_setPracticeActivity(result.last as PracticeActivityEvent?);
} catch (e, s) {
_setPracticeActivity(null);
debugger(when: kDebugMode);
ErrorHandler.logError(
e: e,
s: s,
m: 'Failed to get new activity',
data: {
'activity': currentActivity,
'record': currentCompletionRecord,
},
);
}
}
void onFlagClick(BuildContext context) {
final TextEditingController feedbackController = TextEditingController();
// } catch (e, s) {
// debugger(when: kDebugMode);
// ErrorHandler.logError(
// e: e,
// s: s,
// m: 'Failed to get new activity',
// data: {
// 'activity': currentActivity,
// 'record': currentCompletionRecord,
// },
// );
// widget.overlayController.exitPracticeFlow();
// }
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text(L10n.of(context)!.reportContentIssueTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(L10n.of(context)!.reportContentIssueDescription),
const SizedBox(height: 10),
TextField(
controller: feedbackController,
decoration: InputDecoration(
labelText: L10n.of(context)!.feedback,
border: const OutlineInputBorder(),
),
maxLines: 3,
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(); // Close the dialog
},
child: Text(L10n.of(context)!.cancel),
),
ElevatedButton(
onPressed: () {
// Call the additional callback function
submitFeedback(feedbackController.text);
Navigator.of(context).pop(); // Close the dialog
},
child: Text(L10n.of(context)!.submit),
),
],
);
},
);
}
/// clear the current activity, record, and selection
/// fetch a new activity, including the offending activity in the request
void submitFeedback(String feedback) {
if (currentActivity == null) {
debugger(when: kDebugMode);
return;
}
_fetchNewActivity(
ActivityQualityFeedback(
feedbackText: feedback,
badActivity: currentActivity!.practiceActivity,
),
).then((activity) {
_setPracticeActivity(activity);
}).catchError((onError) {
debugger(when: kDebugMode);
ErrorHandler.logError(
e: onError,
m: 'Failed to get new activity',
data: {
'activity': currentActivity,
'record': currentCompletionRecord,
},
);
widget.overlayController.exitPracticeFlow();
});
// clear the current activity and record
currentActivity = null;
currentCompletionRecord = null;
}
RepresentationEvent? get representation =>
@ -300,18 +381,21 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
return GamifiedTextWidget(userMessage: userMessage!);
}
return Stack(
return Container(
constraints: const BoxConstraints(
maxWidth: 350,
minWidth: 350,
),
child: Stack(
alignment: Alignment.center,
children: [
// Main content
const Positioned(
child: PointsGainedAnimation(),
),
Column(
children: [
activityWidget,
// navigationButtons,
],
Container(
padding: const EdgeInsets.all(8),
child: activityWidget,
),
// Conditionally show the darkening and progress indicator based on the loading state
if (!savoringTheJoy && fetchingActivity) ...[
@ -324,96 +408,25 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
child: CircularProgressIndicator(),
),
],
// Flag button in the top right corner
Positioned(
top: 0,
right: 0,
child: Opacity(
opacity: 0.8, // Slight opacity
child: Tooltip(
message: L10n.of(context)!.reportContentIssueTitle,
child: IconButton(
icon: const Icon(Icons.flag),
iconSize: 16,
onPressed: () =>
currentActivity == null ? null : onFlagClick(context),
),
),
),
),
],
),
);
}
}
/// Seperated out the target tokens from the practice activity card
/// in order to control the state of the target tokens
class TargetTokensController {
List<TokenWithXP>? _targetTokens;
TargetTokensController();
/// From the tokens in the message, do a preliminary filtering of which to target
/// Then get the construct uses for those tokens
Future<List<TokenWithXP>> targetTokens(
BuildContext context,
PangeaMessageEvent pangeaMessageEvent,
) async {
if (_targetTokens != null) {
return _targetTokens!;
}
_targetTokens = await _initialize(context, pangeaMessageEvent);
await updateTokensWithConstructs(
MatrixState.pangeaController.analytics.analyticsStream.value ?? [],
context,
pangeaMessageEvent,
);
return _targetTokens!;
}
Future<List<TokenWithXP>> _initialize(
BuildContext context,
PangeaMessageEvent pangeaMessageEvent,
) async {
if (!context.mounted) {
ErrorHandler.logError(
m: 'getTargetTokens called when not mounted',
s: StackTrace.current,
);
return _targetTokens = [];
}
final tokens = await pangeaMessageEvent
.representationByLanguage(pangeaMessageEvent.messageDisplayLangCode)
?.tokensGlobal(context);
if (tokens == null || tokens.isEmpty) {
debugger(when: kDebugMode);
return _targetTokens = [];
}
_targetTokens = [];
for (int i = 0; i < tokens.length; i++) {
//don't bother with tokens that we don't save to vocab
if (!tokens[i].lemma.saveVocab) {
continue;
}
_targetTokens!.add(tokens[i].emptyTokenWithXP);
}
return _targetTokens!;
}
Future<void> updateTokensWithConstructs(
List<OneConstructUse> constructUses,
context,
pangeaMessageEvent,
) async {
final ConstructListModel constructList = ConstructListModel(
uses: constructUses,
type: null,
);
_targetTokens ??= await _initialize(context, pangeaMessageEvent);
for (final token in _targetTokens!) {
for (final construct in token.constructs) {
final constructUseModel = constructList.getConstructUses(
construct.id.lemma,
construct.id.type,
);
if (constructUseModel != null) {
construct.xp += constructUseModel.points;
construct.lastUsed = constructUseModel.lastUsed;
}
}
}
}
}

@ -0,0 +1,99 @@
import 'dart:developer';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
/// Seperated out the target tokens from the practice activity card
/// in order to control the state of the target tokens
class TargetTokensController {
List<TokenWithXP>? _targetTokens;
TargetTokensController();
/// From the tokens in the message, do a preliminary filtering of which to target
/// Then get the construct uses for those tokens
Future<List<TokenWithXP>> targetTokens(
BuildContext context,
PangeaMessageEvent pangeaMessageEvent,
) async {
if (_targetTokens != null) {
return _targetTokens!;
}
_targetTokens = await _initialize(context, pangeaMessageEvent);
await updateTokensWithConstructs(
MatrixState.pangeaController.analytics.analyticsStream.value ?? [],
context,
pangeaMessageEvent,
);
return _targetTokens!;
}
Future<List<TokenWithXP>> _initialize(
BuildContext context,
PangeaMessageEvent pangeaMessageEvent,
) async {
if (!context.mounted) {
ErrorHandler.logError(
m: 'getTargetTokens called when not mounted',
s: StackTrace.current,
);
return _targetTokens = [];
}
final tokens = await pangeaMessageEvent
.representationByLanguage(pangeaMessageEvent.messageDisplayLangCode)
?.tokensGlobal(context);
if (tokens == null || tokens.isEmpty) {
debugger(when: kDebugMode);
return _targetTokens = [];
}
_targetTokens = [];
for (int i = 0; i < tokens.length; i++) {
//don't bother with tokens that we don't save to vocab
if (!tokens[i].lemma.saveVocab) {
continue;
}
_targetTokens!.add(tokens[i].emptyTokenWithXP);
}
return _targetTokens!;
}
Future<void> updateTokensWithConstructs(
List<OneConstructUse> constructUses,
context,
pangeaMessageEvent,
) async {
final ConstructListModel constructList = ConstructListModel(
uses: constructUses,
type: null,
);
_targetTokens ??= await _initialize(context, pangeaMessageEvent);
for (final token in _targetTokens!) {
for (final construct in token.constructs) {
final constructUseModel = constructList.getConstructUses(
construct.id.lemma,
construct.id.type,
);
if (constructUseModel != null) {
construct.xp += constructUseModel.points;
construct.lastUsed = constructUseModel.lastUsed;
}
}
}
}
}

@ -1305,18 +1305,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.dev"
source: hosted
version: "10.0.4"
version: "10.0.5"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
version: "3.0.5"
leak_tracker_testing:
dependency: transitive
description:
@ -1417,10 +1417,10 @@ packages:
dependency: transitive
description:
name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.8.0"
version: "0.11.1"
material_symbols_icons:
dependency: "direct main"
description:
@ -1442,10 +1442,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.15.0"
mgrs_dart:
dependency: transitive
description:
@ -1682,10 +1682,10 @@ packages:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
version: "3.1.5"
platform_detect:
dependency: transitive
description:
@ -2303,26 +2303,26 @@ packages:
dependency: transitive
description:
name: test
sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073"
sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e"
url: "https://pub.dev"
source: hosted
version: "1.25.2"
version: "1.25.7"
test_api:
dependency: transitive
description:
name: test_api
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
version: "0.7.2"
test_core:
dependency: transitive
description:
name: test_core
sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4"
sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
version: "0.6.4"
timezone:
dependency: transitive
description:
@ -2615,10 +2615,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
source: hosted
version: "14.2.1"
version: "14.2.5"
wakelock_plus:
dependency: "direct main"
description:

Loading…
Cancel
Save