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",
"practice": "Practice",
"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:",
"speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores",
"previous": "Previous",

@ -4513,6 +4513,7 @@
"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.",
"presenceStyle": "Presencia:",
"noActivitiesFound": "¡Ya has practicado por ahora! Vuelve más tarde para ver más.",
"presencesToggle": "Mostrar mensajes de estado de otros usuarios",
"writeAMessageFlag": "Escribe un mensaje en {l1flag} o {l2flag}",
"@writeAMessageFlag": {

@ -22,7 +22,7 @@ void main() async {
// #Pangea
try {
await dotenv.load(fileName: ".env");
await dotenv.load(fileName: ".env.local_choreo");
} catch (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/models/analytics/constructs_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';
class PracticeActivityRecordModel {
@ -85,50 +84,17 @@ class PracticeActivityRecordModel {
/// 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 method iterates over the [tgtConstructs] of the [practiceActivity] and creates a [OneConstructUse] object for each construct and useType.
List<OneConstructUse> uses(
/// The method iterates over the [responses] to get [OneConstructUse] objects for each
List<OneConstructUse> usesForAllResponses(
PracticeActivityModel practiceActivity,
ConstructUseMetaData metadata,
) {
try {
final List<OneConstructUse> uses = [];
final uniqueResponses = responses.toSet();
final List<ConstructUseTypeEnum> useTypes =
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 [];
}
}
) =>
responses
.toSet()
.expand(
(response) => response.toUses(practiceActivity, metadata),
)
.toList();
@override
bool operator ==(Object other) {
@ -164,9 +130,26 @@ class ActivityRecordResponse {
required this.timestamp,
});
//TODO - differentiate into different activity types
ConstructUseTypeEnum get useType =>
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) {
return ActivityRecordResponse(
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
/// 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
/// 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;
int activitiesLeftToComplete = neededActivities;
@override
void initState() {
@ -72,8 +65,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
duration: FluffyThemes.animationDuration,
);
activitiesLeftToComplete =
needed - widget._pangeaMessageEvent.numberOfActivitiesCompleted;
activitiesLeftToComplete = activitiesLeftToComplete -
widget._pangeaMessageEvent.numberOfActivitiesCompleted;
setInitialToolbarMode();
}
@ -84,7 +77,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
/// and check if the toolbar should be unlocked
void onActivityFinish() {
if (!mounted) return;
completedThisSession += 1;
activitiesLeftToComplete -= 1;
clearSelection();
setState(() {});
@ -95,8 +87,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
void exitPracticeFlow() {
debugPrint('Exiting practice flow');
clearSelection();
needed = 0;
setInitialToolbarMode();
activitiesLeftToComplete = 0;
setState(() {});
}

@ -1,9 +1,14 @@
import 'dart:developer';
import 'package:collection/collection.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/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/widgets/practice_activity/practice_activity_card.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
/// The multiple choice activity view
@ -27,13 +32,9 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
PracticeActivityRecordModel? get currentRecordModel =>
widget.practiceCardController.currentCompletionRecord;
// bool get isSubmitted =>
// widget.currentActivity?.latestUserRecord?.record.latestResponse != null;
@override
void initState() {
super.initState();
// setCompletionRecord();
}
@override
@ -42,34 +43,10 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
if (widget.practiceCardController.currentCompletionRecord?.responses
.isEmpty ??
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) {
if (currentRecordModel?.hasTextResponse(value) ?? false) {
return;
@ -79,22 +56,29 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
.currentActivity!.practiceActivity.multipleChoice!
.isCorrect(value, index);
// final ConstructUseTypeEnum useType =
// isCorrect ? ConstructUseTypeEnum.corPA : ConstructUseTypeEnum.incPA;
currentRecordModel?.addResponse(
text: value,
score: isCorrect ? 1 : 0,
);
// TODO - add draft uses
// activities currently pass around tgtConstructs but not the token
// either we change addDraftUses to take constructs or we get and pass the token
// MatrixState.pangeaController.myAnalytics.addDraftUses(
// widget.currentActivity.practiceActivity.tg,
// widget.practiceCardController.widget.pangeaMessageEvent.room.id,
// useType,
// );
if (currentRecordModel == null ||
currentRecordModel!.latestResponse == null) {
debugger(when: kDebugMode);
return;
}
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 (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:developer';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/enum/activity_type_enum.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/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/widgets/matrix.dart';
import 'package:flutter/foundation.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
// while simultaneously fetching a new activity and not showing the loading spinner
// 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;
Timer? joyTimer;
@override
void initState() {
@ -62,30 +61,28 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
initialize();
}
@override
void dispose() {
joyTimer?.cancel();
super.dispose();
}
void _updateFetchingActivity(bool value) {
if (fetchingActivity == value) return;
setState(() => fetchingActivity = value);
}
void _setPracticeActivity(PracticeActivityEvent? activity) {
//set elsewhere but just in case
fetchingActivity = false;
currentActivity = activity;
if (activity == null) {
widget.overlayController.exitPracticeFlow();
return;
}
currentActivity = activity;
//make new completion record
currentCompletionRecord = PracticeActivityRecordModel(
question: activity.practiceActivity.question,
);
widget.overlayController.setSelectedSpan(currentActivity!.practiceActivity);
widget.overlayController.setSelectedSpan(activity.practiceActivity);
}
/// Get an existing activity if there is one.
@ -168,19 +165,26 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
}
}
void setCompletionRecord(PracticeActivityRecordModel? recordModel) {
currentCompletionRecord = recordModel;
}
ConstructUseMetaData get metadata => ConstructUseMetaData(
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 {
joyTimer?.cancel();
if (savoringTheJoy) return;
if (savoringTheJoy) {
//should not happen
debugger(when: kDebugMode);
}
savoringTheJoy = true;
joyTimer = Timer(appropriateTimeForJoy, () {
savoringTheJoy = false;
joyTimer?.cancel();
});
debugPrint('Savoring the joy');
await Future.delayed(appropriateTimeForJoy);
savoringTheJoy = false;
debugPrint('Savoring the joy is over');
}
/// Called when the user finishes an activity.
@ -194,33 +198,17 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
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
// NOTE - multiple choice activity is handling adding these to analytics
await targetTokensController.updateTokensWithConstructs(
uses,
currentCompletionRecord!.usesForAllResponses(
currentActivity!.practiceActivity,
metadata,
),
context,
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
// send a copy of the activity record to make sure its not overwritten by
// the new activity
@ -240,7 +228,12 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
widget.overlayController.onActivityFinish();
_setPracticeActivity(await _fetchNewActivity());
final Iterable<dynamic> result = await Future.wait([
_savorTheJoy(),
_fetchNewActivity(),
]);
_setPracticeActivity(result.last as PracticeActivityEvent?);
// } catch (e, s) {
// debugger(when: kDebugMode);
@ -295,12 +288,7 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
}
String? get userMessage {
// if the user has finished all the activities to unlock the toolbar in this session
if (widget.overlayController.finishedActivitiesThisSession) {
return "Boom! Tools unlocked!";
// if we have no activities to show
} else if (!fetchingActivity && currentActivity == null) {
if (!fetchingActivity && currentActivity == null) {
return L10n.of(context)!.noActivitiesFound;
}
return null;
@ -309,20 +297,7 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
@override
Widget build(BuildContext context) {
if (userMessage != null) {
return Center(
child: Container(
constraints: const BoxConstraints(
minHeight: 80,
),
child: Text(
userMessage!,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
);
return GamifiedTextWidget(userMessage: userMessage!);
}
return Stack(
@ -435,7 +410,7 @@ class TargetTokensController {
construct.id.type,
);
if (constructUseModel != null) {
construct.xp = constructUseModel.points;
construct.xp += constructUseModel.points;
construct.lastUsed = constructUseModel.lastUsed;
}
}

Loading…
Cancel
Save