diff --git a/lib/pangea/choreographer/controllers/alternative_translator.dart b/lib/pangea/choreographer/controllers/alternative_translator.dart index e26bc3aa1..550acef25 100644 --- a/lib/pangea/choreographer/controllers/alternative_translator.dart +++ b/lib/pangea/choreographer/controllers/alternative_translator.dart @@ -3,11 +3,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:http/http.dart' as http; +import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import '../repo/similarity_repo.dart'; class AlternativeTranslator { @@ -19,6 +23,7 @@ class AlternativeTranslator { FeedbackKey? translationFeedbackKey; List translations = []; SimilartyResponseModel? similarityResponse; + AlternativeTranslator(this.choreographer); void clear() { @@ -97,28 +102,86 @@ class AlternativeTranslator { } } - List get _selectedTokens => choreographer.choreoRecord.itSteps - .where((step) => step.chosenContinuance != null) - .map( - (step) => step.chosenContinuance!.tokens.where( - (token) => token.lemma.saveVocab, - ), - ) - .expand((element) => element) - .toList(); + List get _itStepConstructs { + final metadata = ConstructUseMetaData( + roomId: choreographer.roomId, + timeStamp: DateTime.now(), + ); - int countVocabularyWordsFromSteps() => - _selectedTokens.map((t) => t.lemma.text.toLowerCase()).toSet().length; + final List constructs = []; + for (final step in choreographer.choreoRecord.itSteps) { + for (final continuance in step.continuances) { + final ConstructUseTypeEnum useType = continuance.wasClicked && + continuance.level == ChoreoConstants.levelThresholdForGreen + ? ConstructUseTypeEnum.corIt + : continuance.wasClicked + ? ConstructUseTypeEnum.incIt + : ConstructUseTypeEnum.ignIt; - int countGrammarConstructsFromSteps() => _selectedTokens - .map( - (t) => t.morph.entries.map( - (m) => "${m.key}:${m.value}".toLowerCase(), - ), - ) - .expand((m) => m) - .toSet() - .length; + final tokens = continuance.tokens.where((t) => t.lemma.saveVocab); + constructs.addAll( + tokens.map( + (token) => OneConstructUse( + useType: useType, + lemma: token.lemma.text, + constructType: ConstructTypeEnum.vocab, + metadata: metadata, + category: token.pos, + form: token.text.content, + ), + ), + ); + for (final token in tokens) { + constructs.add( + OneConstructUse( + useType: useType, + lemma: token.pos, + form: token.text.content, + category: "POS", + constructType: ConstructTypeEnum.morph, + metadata: metadata, + ), + ); + for (final entry in token.morph.entries) { + constructs.add( + OneConstructUse( + useType: useType, + lemma: entry.value, + form: token.text.content, + category: entry.key, + constructType: ConstructTypeEnum.morph, + metadata: metadata, + ), + ); + } + } + } + } + return constructs; + } + + int countNewConstructs(ConstructTypeEnum type) { + final vocabUses = _itStepConstructs.where((c) => c.constructType == type); + final Map constructPoints = {}; + for (final use in vocabUses) { + constructPoints[use.identifier] ??= 0; + constructPoints[use.identifier] = + constructPoints[use.identifier]! + use.pointValue; + } + + final constructListModel = + MatrixState.pangeaController.getAnalytics.constructListModel; + + int newConstructCount = 0; + for (final entry in constructPoints.entries) { + final construct = constructListModel.getConstructUses(entry.key); + if (construct?.points == entry.value) { + newConstructCount++; + } + } + + return newConstructCount; + } String getDefaultFeedback(BuildContext context) { final l10n = L10n.of(context); diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index d03759f49..62da52de0 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -4,6 +4,7 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; @@ -232,10 +233,14 @@ class ITBarState extends State with SingleTickerProviderStateMixin { controller: itController, vocabCount: itController .choreographer.altTranslator - .countVocabularyWordsFromSteps(), + .countNewConstructs( + ConstructTypeEnum.vocab, + ), grammarCount: itController .choreographer.altTranslator - .countGrammarConstructsFromSteps(), + .countNewConstructs( + ConstructTypeEnum.morph, + ), feedbackText: itController .choreographer.altTranslator .getDefaultFeedback(context), diff --git a/lib/pangea/choreographer/widgets/translation_finished_flow.dart b/lib/pangea/choreographer/widgets/translation_finished_flow.dart index 8e157ca93..bc10f9de8 100644 --- a/lib/pangea/choreographer/widgets/translation_finished_flow.dart +++ b/lib/pangea/choreographer/widgets/translation_finished_flow.dart @@ -9,12 +9,12 @@ import '../../bot/utils/bot_style.dart'; import '../../common/utils/error_handler.dart'; import '../controllers/it_controller.dart'; -class TranslationFeedback extends StatelessWidget { +class TranslationFeedback extends StatefulWidget { final int vocabCount; final int grammarCount; final String feedbackText; - final ITController controller; + const TranslationFeedback({ super.key, required this.controller, @@ -23,65 +23,231 @@ class TranslationFeedback extends StatelessWidget { required this.feedbackText, }); + @override + State createState() => _TranslationFeedbackState(); +} + +class _TranslationFeedbackState extends State + with TickerProviderStateMixin { + late final int starRating; + late final int vocabCount; + late final int grammarCount; + + // Animation controllers for each component + late AnimationController _starsController; + late AnimationController _vocabController; + late AnimationController _grammarController; + + // Animations for opacity and scale + late Animation _starsOpacity; + late Animation _starsScale; + late Animation _vocabOpacity; + late Animation _grammarOpacity; + + // Constants for animation timing + static const vocabDelay = Duration(milliseconds: 800); + static const grammarDelay = Duration(milliseconds: 1400); + + // Duration for each individual animation + static const elementAnimDuration = Duration(milliseconds: 800); + + @override + void initState() { + super.initState(); + vocabCount = widget.vocabCount; + grammarCount = widget.grammarCount; + + final altTranslator = widget.controller.choreographer.altTranslator; + starRating = altTranslator.starRating; + + // Initialize animation controllers + _starsController = AnimationController( + vsync: this, + duration: elementAnimDuration, + ); + + _vocabController = AnimationController( + vsync: this, + duration: elementAnimDuration, + ); + + _grammarController = AnimationController( + vsync: this, + duration: elementAnimDuration, + ); + + // Define animations + _starsOpacity = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _starsController, curve: Curves.easeInOut), + ); + + _starsScale = Tween(begin: 0.5, end: 1.0).animate( + CurvedAnimation(parent: _starsController, curve: Curves.elasticOut), + ); + + _vocabOpacity = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _vocabController, curve: Curves.easeInOut), + ); + + _grammarOpacity = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _grammarController, curve: Curves.easeInOut), + ); + + // Start animations with appropriate delays + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + // Start stars animation immediately + _starsController.forward(); + + // Start vocab animation after delay if there's vocab to show + if (vocabCount > 0) { + Future.delayed(vocabDelay, () { + if (mounted) _vocabController.forward(); + }); + } + + // Start grammar animation after delay if there's grammar to show + if (grammarCount > 0) { + Future.delayed(grammarDelay, () { + if (mounted) _grammarController.forward(); + }); + } + } + }); + } + + @override + void dispose() { + _starsController.dispose(); + _vocabController.dispose(); + _grammarController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - final altTranslator = controller.choreographer.altTranslator; try { return Column( - spacing: 16.0, + mainAxisSize: MainAxisSize.min, children: [ - FillingStars(rating: altTranslator.starRating), + // Animated stars + AnimatedBuilder( + animation: _starsController, + builder: (context, child) { + return Opacity( + opacity: _starsOpacity.value, + child: Transform.scale( + scale: _starsScale.value, + child: Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: FillingStars(rating: starRating), + ), + ), + ); + }, + ), + if (vocabCount > 0 || grammarCount > 0) - Row( - mainAxisSize: MainAxisSize.min, - spacing: 16.0, - children: [ - if (vocabCount > 0) - Row( - spacing: 8.0, - children: [ - Icon( - Symbols.dictionary, - color: ProgressIndicatorEnum.wordsUsed.color(context), - size: 24, - ), - Text( - "+ $vocabCount", - style: TextStyle( - color: ProgressIndicatorEnum.wordsUsed.color(context), - fontWeight: FontWeight.bold, - ), - ), - ], - ), - if (grammarCount > 0) - Row( - spacing: 8.0, - children: [ - Icon( - Symbols.toys_and_games, - color: ProgressIndicatorEnum.morphsUsed.color(context), - size: 24, - ), - Text( - "+ $grammarCount", - style: TextStyle( - color: - ProgressIndicatorEnum.morphsUsed.color(context), - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ], + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (vocabCount > 0) + AnimatedBuilder( + animation: _vocabController, + builder: (context, child) { + return Opacity( + opacity: _vocabOpacity.value, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Symbols.dictionary, + color: ProgressIndicatorEnum.wordsUsed + .color(context), + size: 24, + ), + const SizedBox(width: 4.0), + AnimatedCounter( + key: const ValueKey("vocabCounter"), + endValue: vocabCount, + // Only start counter animation when opacity animation is complete + startAnimation: _vocabOpacity.value > 0.9, + style: TextStyle( + color: ProgressIndicatorEnum.wordsUsed + .color(context), + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ); + }, + ), + if (grammarCount > 0) + AnimatedBuilder( + animation: _grammarController, + builder: (context, child) { + return Opacity( + opacity: _grammarOpacity.value, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Symbols.toys_and_games, + color: ProgressIndicatorEnum.morphsUsed + .color(context), + size: 24, + ), + const SizedBox(width: 4.0), + AnimatedCounter( + key: const ValueKey("grammarCounter"), + endValue: grammarCount, + // Only start counter animation when opacity animation is complete + startAnimation: _grammarOpacity.value > 0.9, + style: TextStyle( + color: ProgressIndicatorEnum.morphsUsed + .color(context), + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ); + }, + ), + ], + ), ) else - Text( - feedbackText, - textAlign: TextAlign.center, - style: BotStyle.text(context), + AnimatedBuilder( + animation: _starsController, + builder: (context, child) { + return Opacity( + opacity: _starsOpacity.value, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + widget.feedbackText, + textAlign: TextAlign.center, + style: BotStyle.text(context), + ), + ), + ); + }, ), - const SizedBox(height: 16.0), + + const SizedBox(height: 8.0), ], ); } catch (err, stack) { @@ -91,9 +257,123 @@ class TranslationFeedback extends StatelessWidget { s: stack, data: {}, ); - // Fallback to a simple message if anything goes wrong return Center(child: Text(L10n.of(context).niceJob)); } } } + +class AnimatedCounter extends StatefulWidget { + final int endValue; + final TextStyle? style; + final Duration duration; + final String prefix; + final bool startAnimation; + + const AnimatedCounter({ + super.key, + required this.endValue, + this.style, + this.duration = const Duration(milliseconds: 1500), + this.prefix = "+ ", + this.startAnimation = true, + }); + + @override + State createState() => _AnimatedCounterState(); +} + +class _AnimatedCounterState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + bool _hasAnimated = false; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: widget.duration, + ); + + _animation = IntTween( + begin: 0, + end: widget.endValue, + ).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeOutCubic, + ), + ); + + // Only start animation if startAnimation is true + if (widget.startAnimation) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _controller.forward(); + _hasAnimated = true; + } + }); + } + } + + @override + void didUpdateWidget(AnimatedCounter oldWidget) { + super.didUpdateWidget(oldWidget); + + // Start animation when startAnimation changes to true + if (!oldWidget.startAnimation && widget.startAnimation && !_hasAnimated) { + _controller.forward(); + _hasAnimated = true; + } + + if (oldWidget.endValue != widget.endValue) { + if (_hasAnimated) { + _animation = IntTween( + begin: _animation.value, + end: widget.endValue, + ).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeOutCubic, + ), + ); + + _controller.forward(from: 0.0); + } else if (widget.startAnimation) { + _animation = IntTween( + begin: 0, + end: widget.endValue, + ).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeOutCubic, + ), + ); + + _controller.forward(); + _hasAnimated = true; + } + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Text( + "${widget.prefix}${_animation.value}", + style: widget.style, + ); + }, + ); + } +}