Toolbar practice (#704)

* remove print statement

* ending animation, savoring joy, properly adding xp in session
pull/1384/head
wcjord 1 year ago committed by GitHub
parent 08a7c74b4a
commit 51e8c4b7ce
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", "suggestToSpaceDesc": "Suggested sub spaces will appear in their main space's chat list",
"practice": "Practice", "practice": "Practice",
"noLanguagesSet": "No languages set", "noLanguagesSet": "No languages set",
"noActivitiesFound": "No practice activities found for this message", "noActivitiesFound": "You're all practiced for now! Come back later for more.",
"hintTitle": "Hint:", "hintTitle": "Hint:",
"speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores", "speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores",
"previous": "Previous", "previous": "Previous",

@ -4513,6 +4513,7 @@
"autoPlayTitle": "Reproducción automática de mensajes", "autoPlayTitle": "Reproducción automática de mensajes",
"autoPlayDesc": "Cuando está activado, el audio de texto a voz de los mensajes se reproducirá automáticamente cuando se seleccione.", "autoPlayDesc": "Cuando está activado, el audio de texto a voz de los mensajes se reproducirá automáticamente cuando se seleccione.",
"presenceStyle": "Presencia:", "presenceStyle": "Presencia:",
"noActivitiesFound": "¡Ya has practicado por ahora! Vuelve más tarde para ver más.",
"presencesToggle": "Mostrar mensajes de estado de otros usuarios", "presencesToggle": "Mostrar mensajes de estado de otros usuarios",
"writeAMessageFlag": "Escribe un mensaje en {l1flag} o {l2flag}", "writeAMessageFlag": "Escribe un mensaje en {l1flag} o {l2flag}",
"@writeAMessageFlag": { "@writeAMessageFlag": {

@ -22,7 +22,7 @@ void main() async {
// #Pangea // #Pangea
try { try {
await dotenv.load(fileName: ".env"); await dotenv.load(fileName: ".env.local_choreo");
} catch (e) { } catch (e) {
Logs().e('Failed to load .env file', e); Logs().e('Failed to load .env file', e);
} }

@ -7,7 +7,6 @@ import 'dart:developer';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
class PracticeActivityRecordModel { class PracticeActivityRecordModel {
@ -85,50 +84,17 @@ class PracticeActivityRecordModel {
/// The [practiceActivity] parameter is the parent event, representing the activity itself. /// The [practiceActivity] parameter is the parent event, representing the activity itself.
/// The [metadata] parameter is the metadata for the construct use, used if the record event isn't available. /// The [metadata] parameter is the metadata for the construct use, used if the record event isn't available.
/// ///
/// The method iterates over the [tgtConstructs] of the [practiceActivity] and creates a [OneConstructUse] object for each construct and useType. /// The method iterates over the [responses] to get [OneConstructUse] objects for each
List<OneConstructUse> uses( List<OneConstructUse> usesForAllResponses(
PracticeActivityModel practiceActivity, PracticeActivityModel practiceActivity,
ConstructUseMetaData metadata, ConstructUseMetaData metadata,
) { ) =>
try { responses
final List<OneConstructUse> uses = []; .toSet()
.expand(
final uniqueResponses = responses.toSet(); (response) => response.toUses(practiceActivity, metadata),
)
final List<ConstructUseTypeEnum> useTypes = .toList();
uniqueResponses.map((response) => response.useType).toList();
for (final construct in practiceActivity.tgtConstructs) {
for (final useType in useTypes) {
uses.add(
OneConstructUse(
lemma: construct.lemma,
constructType: construct.type,
useType: useType,
//TODO - find form of construct within the message
//this is related to the feature of highlighting the target construct in the message
form: construct.lemma,
metadata: metadata,
),
);
}
}
return uses;
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(
e: e,
s: s,
data: {
'recordModel': toJson(),
'practiceActivity': practiceActivity,
'metadata': metadata,
},
);
return [];
}
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
@ -164,9 +130,26 @@ class ActivityRecordResponse {
required this.timestamp, required this.timestamp,
}); });
//TODO - differentiate into different activity types
ConstructUseTypeEnum get useType => ConstructUseTypeEnum get useType =>
score > 0 ? ConstructUseTypeEnum.corPA : ConstructUseTypeEnum.incPA; score > 0 ? ConstructUseTypeEnum.corPA : ConstructUseTypeEnum.incPA;
// for each target construct create a OneConstructUse object
List<OneConstructUse> toUses(
PracticeActivityModel practiceActivity,
ConstructUseMetaData metadata,
) =>
practiceActivity.tgtConstructs
.map(
(construct) => OneConstructUse(
lemma: construct.lemma,
constructType: construct.type,
useType: useType,
metadata: metadata,
),
)
.toList();
factory ActivityRecordResponse.fromJson(Map<String, dynamic> json) { factory ActivityRecordResponse.fromJson(Map<String, dynamic> json) {
return ActivityRecordResponse( return ActivityRecordResponse(
text: json['text'] as String?, text: json['text'] as String?,

@ -53,16 +53,9 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
/// The number of activities that need to be completed before the toolbar is unlocked /// The number of activities that need to be completed before the toolbar is unlocked
/// If we don't have any good activities for them, we'll decrease this number /// If we don't have any good activities for them, we'll decrease this number
int needed = 3; static const int neededActivities = 3;
/// Whether the user has completed the activities needed to unlock the toolbar int activitiesLeftToComplete = neededActivities;
/// within this overlay 'session'. if they click out and come back in then
/// we can give them some more activities to complete
int completedThisSession = 0;
bool get finishedActivitiesThisSession => completedThisSession >= needed;
late int activitiesLeftToComplete = needed;
@override @override
void initState() { void initState() {
@ -72,8 +65,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
duration: FluffyThemes.animationDuration, duration: FluffyThemes.animationDuration,
); );
activitiesLeftToComplete = activitiesLeftToComplete = activitiesLeftToComplete -
needed - widget._pangeaMessageEvent.numberOfActivitiesCompleted; widget._pangeaMessageEvent.numberOfActivitiesCompleted;
setInitialToolbarMode(); setInitialToolbarMode();
} }
@ -84,7 +77,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
/// and check if the toolbar should be unlocked /// and check if the toolbar should be unlocked
void onActivityFinish() { void onActivityFinish() {
if (!mounted) return; if (!mounted) return;
completedThisSession += 1;
activitiesLeftToComplete -= 1; activitiesLeftToComplete -= 1;
clearSelection(); clearSelection();
setState(() {}); setState(() {});
@ -95,8 +87,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
void exitPracticeFlow() { void exitPracticeFlow() {
debugPrint('Exiting practice flow'); debugPrint('Exiting practice flow');
clearSelection(); clearSelection();
needed = 0; activitiesLeftToComplete = 0;
setInitialToolbarMode();
setState(() {}); setState(() {});
} }

@ -1,9 +1,14 @@
import 'dart:developer';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart'; import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// The multiple choice activity view /// The multiple choice activity view
@ -27,13 +32,9 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
PracticeActivityRecordModel? get currentRecordModel => PracticeActivityRecordModel? get currentRecordModel =>
widget.practiceCardController.currentCompletionRecord; widget.practiceCardController.currentCompletionRecord;
// bool get isSubmitted =>
// widget.currentActivity?.latestUserRecord?.record.latestResponse != null;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// setCompletionRecord();
} }
@override @override
@ -42,34 +43,10 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
if (widget.practiceCardController.currentCompletionRecord?.responses if (widget.practiceCardController.currentCompletionRecord?.responses
.isEmpty ?? .isEmpty ??
false) { false) {
selectedChoiceIndex = null; setState(() => selectedChoiceIndex = null);
} }
} }
/// Sets the completion record for the multiple choice activity.
/// If the user record is null, it creates a new record model with the question
/// from the current activity and sets the selected choice index to null.
/// Otherwise, it sets the current model to the user record's record and
/// determines the selected choice index.
void setCompletionRecord() {
// if (widget.currentActivity?.latestUserRecord?.record == null) {
widget.practiceCardController.setCompletionRecord(
PracticeActivityRecordModel(
question:
widget.currentActivity?.practiceActivity.multipleChoice!.question,
),
);
selectedChoiceIndex = null;
// } else {
// widget.practiceCardController.setCompletionRecord(
// widget.currentActivity!.latestUserRecord!.record);
// selectedChoiceIndex = widget
// .currentActivity?.practiceActivity.multipleChoice!
// .choiceIndex(currentRecordModel!.latestResponse!.text!);
// }
setState(() {});
}
void updateChoice(String value, int index) { void updateChoice(String value, int index) {
if (currentRecordModel?.hasTextResponse(value) ?? false) { if (currentRecordModel?.hasTextResponse(value) ?? false) {
return; return;
@ -79,22 +56,29 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
.currentActivity!.practiceActivity.multipleChoice! .currentActivity!.practiceActivity.multipleChoice!
.isCorrect(value, index); .isCorrect(value, index);
// final ConstructUseTypeEnum useType =
// isCorrect ? ConstructUseTypeEnum.corPA : ConstructUseTypeEnum.incPA;
currentRecordModel?.addResponse( currentRecordModel?.addResponse(
text: value, text: value,
score: isCorrect ? 1 : 0, score: isCorrect ? 1 : 0,
); );
// TODO - add draft uses if (currentRecordModel == null ||
// activities currently pass around tgtConstructs but not the token currentRecordModel!.latestResponse == null) {
// either we change addDraftUses to take constructs or we get and pass the token debugger(when: kDebugMode);
// MatrixState.pangeaController.myAnalytics.addDraftUses( return;
// widget.currentActivity.practiceActivity.tg, }
// widget.practiceCardController.widget.pangeaMessageEvent.room.id,
// useType, MatrixState.pangeaController.myAnalytics.setState(
// ); AnalyticsStream(
// note - this maybe should be the activity event id
eventId:
widget.practiceCardController.widget.pangeaMessageEvent.eventId,
roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id,
constructs: currentRecordModel!.latestResponse!.toUses(
widget.practiceCardController.currentActivity!.practiceActivity,
widget.practiceCardController.metadata,
),
),
);
// If the selected choice is correct, send the record and get the next activity // If the selected choice is correct, send the record and get the next activity
if (widget.currentActivity!.practiceActivity.multipleChoice! if (widget.currentActivity!.practiceActivity.multipleChoice!

@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
class StarAnimationWidget extends StatefulWidget {
const StarAnimationWidget({super.key});
@override
_StarAnimationWidgetState createState() => _StarAnimationWidgetState();
}
class _StarAnimationWidgetState extends State<StarAnimationWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _opacityAnimation;
late Animation<double> _sizeAnimation;
@override
void initState() {
super.initState();
// Initialize the AnimationController
_controller = AnimationController(
duration: const Duration(seconds: 1), // Duration of the animation
vsync: this,
)..repeat(reverse: true); // Repeat the animation in reverse
// Define the opacity animation
_opacityAnimation =
Tween<double>(begin: 0.8, end: 1.0).animate(_controller);
// Define the size animation
_sizeAnimation = Tween<double>(begin: 56.0, end: 60.0).animate(_controller);
}
@override
void dispose() {
// Dispose of the controller to free resources
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
// Set constant height and width for the star container
height: 80.0,
width: 80.0,
child: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _opacityAnimation.value,
child: Icon(
Icons.star,
color: Colors.amber,
size: _sizeAnimation.value,
),
);
},
),
),
);
}
}
class GamifiedTextWidget extends StatelessWidget {
final String userMessage;
const GamifiedTextWidget({super.key, required this.userMessage});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min, // Adjusts the size to fit children
children: [
// Star animation above the text
const StarAnimationWidget(),
const SizedBox(height: 10), // Spacing between the star and text
Container(
constraints: const BoxConstraints(
minHeight: 80,
),
child: Text(
userMessage,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center, // Center-align the text
),
),
],
),
);
}
}

@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; 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_message_event.dart';
@ -16,6 +15,7 @@ import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/animations/gain_points.dart'; import 'package:fluffychat/pangea/widgets/animations/gain_points.dart';
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.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/multiple_choice_activity.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/no_more_practice_card.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -52,9 +52,8 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
// Used to show an animation when the user completes an activity // Used to show an animation when the user completes an activity
// while simultaneously fetching a new activity and not showing the loading spinner // while simultaneously fetching a new activity and not showing the loading spinner
// until the appropriate time has passed to 'savor the joy' // until the appropriate time has passed to 'savor the joy'
Duration appropriateTimeForJoy = const Duration(milliseconds: 500); Duration appropriateTimeForJoy = const Duration(milliseconds: 1000);
bool savoringTheJoy = false; bool savoringTheJoy = false;
Timer? joyTimer;
@override @override
void initState() { void initState() {
@ -62,30 +61,28 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
initialize(); initialize();
} }
@override
void dispose() {
joyTimer?.cancel();
super.dispose();
}
void _updateFetchingActivity(bool value) { void _updateFetchingActivity(bool value) {
if (fetchingActivity == value) return; if (fetchingActivity == value) return;
setState(() => fetchingActivity = value); setState(() => fetchingActivity = value);
} }
void _setPracticeActivity(PracticeActivityEvent? activity) { void _setPracticeActivity(PracticeActivityEvent? activity) {
//set elsewhere but just in case
fetchingActivity = false;
currentActivity = activity;
if (activity == null) { if (activity == null) {
widget.overlayController.exitPracticeFlow(); widget.overlayController.exitPracticeFlow();
return; return;
} }
currentActivity = activity; //make new completion record
currentCompletionRecord = PracticeActivityRecordModel( currentCompletionRecord = PracticeActivityRecordModel(
question: activity.practiceActivity.question, question: activity.practiceActivity.question,
); );
widget.overlayController.setSelectedSpan(currentActivity!.practiceActivity); widget.overlayController.setSelectedSpan(activity.practiceActivity);
} }
/// Get an existing activity if there is one. /// Get an existing activity if there is one.
@ -168,19 +165,26 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
} }
} }
void setCompletionRecord(PracticeActivityRecordModel? recordModel) { ConstructUseMetaData get metadata => ConstructUseMetaData(
currentCompletionRecord = recordModel; eventId: widget.pangeaMessageEvent.eventId,
} roomId: widget.pangeaMessageEvent.room.id,
timeStamp: DateTime.now(),
);
/// future that simply waits for the appropriate time to savor the joy
Future<void> _savorTheJoy() async { Future<void> _savorTheJoy() async {
joyTimer?.cancel(); if (savoringTheJoy) {
if (savoringTheJoy) return; //should not happen
debugger(when: kDebugMode);
}
savoringTheJoy = true; savoringTheJoy = true;
joyTimer = Timer(appropriateTimeForJoy, () {
debugPrint('Savoring the joy');
await Future.delayed(appropriateTimeForJoy);
savoringTheJoy = false; savoringTheJoy = false;
joyTimer?.cancel();
}); debugPrint('Savoring the joy is over');
} }
/// Called when the user finishes an activity. /// Called when the user finishes an activity.
@ -194,33 +198,17 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
return; return;
} }
// start joy timer
_savorTheJoy();
final uses = currentCompletionRecord!.uses(
currentActivity!.practiceActivity,
ConstructUseMetaData(
roomId: widget.pangeaMessageEvent.room.id,
timeStamp: DateTime.now(),
),
);
// update the target tokens with the new construct uses // update the target tokens with the new construct uses
// NOTE - multiple choice activity is handling adding these to analytics
await targetTokensController.updateTokensWithConstructs( await targetTokensController.updateTokensWithConstructs(
uses, currentCompletionRecord!.usesForAllResponses(
currentActivity!.practiceActivity,
metadata,
),
context, context,
widget.pangeaMessageEvent, widget.pangeaMessageEvent,
); );
MatrixState.pangeaController.myAnalytics.setState(
AnalyticsStream(
// note - this maybe should be the activity event id
eventId: widget.pangeaMessageEvent.eventId,
roomId: widget.pangeaMessageEvent.room.id,
constructs: uses,
),
);
// save the record without awaiting to avoid blocking the UI // save the record without awaiting to avoid blocking the UI
// send a copy of the activity record to make sure its not overwritten by // send a copy of the activity record to make sure its not overwritten by
// the new activity // the new activity
@ -240,7 +228,12 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
widget.overlayController.onActivityFinish(); widget.overlayController.onActivityFinish();
_setPracticeActivity(await _fetchNewActivity()); final Iterable<dynamic> result = await Future.wait([
_savorTheJoy(),
_fetchNewActivity(),
]);
_setPracticeActivity(result.last as PracticeActivityEvent?);
// } catch (e, s) { // } catch (e, s) {
// debugger(when: kDebugMode); // debugger(when: kDebugMode);
@ -295,12 +288,7 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
} }
String? get userMessage { String? get userMessage {
// if the user has finished all the activities to unlock the toolbar in this session if (!fetchingActivity && currentActivity == null) {
if (widget.overlayController.finishedActivitiesThisSession) {
return "Boom! Tools unlocked!";
// if we have no activities to show
} else if (!fetchingActivity && currentActivity == null) {
return L10n.of(context)!.noActivitiesFound; return L10n.of(context)!.noActivitiesFound;
} }
return null; return null;
@ -309,20 +297,7 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (userMessage != null) { if (userMessage != null) {
return Center( return GamifiedTextWidget(userMessage: userMessage!);
child: Container(
constraints: const BoxConstraints(
minHeight: 80,
),
child: Text(
userMessage!,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
);
} }
return Stack( return Stack(
@ -435,7 +410,7 @@ class TargetTokensController {
construct.id.type, construct.id.type,
); );
if (constructUseModel != null) { if (constructUseModel != null) {
construct.xp = constructUseModel.points; construct.xp += constructUseModel.points;
construct.lastUsed = constructUseModel.lastUsed; construct.lastUsed = constructUseModel.lastUsed;
} }
} }

Loading…
Cancel
Save