2353-add-number-animation-xp-earned-in-it-bar (#2363)

* fix(IT Controller): fixed accuracy issues with star calcultion and point calculation for vocab + grammar. Added number animation. Staggered animations

* generated

* chore: redirect to new group page on click new chat button in space view (#2354)

* chore: disable custom message text sizing (#2355)

* feat: initial work to prevent giving points for copy-pasted text (#2345)

* feat: initial work to prevent giving points for copy-pasted text

* chore: replace tokenization with comparing token content with pasted content

* fix(emoji_activity_generator): ensure unique choices

* fix(intl_en): two copy edits

* fix(lemma_meaning_widget): fix text alignment

* chore(practice_selection): preferencing tokens without activities in selection

* 2364 on chat creation with activity if no room image set activity image (#2371)

* chore: formatting

* chore: on chat creation without activity, set avatar to activity image if no image set

* chore: in empty chat popup, listen for changes to participant count and close self if it increases (#2372)

* chore: constrain width of unsubscribed card (#2373)

* chore: fix dialogs in report offensive message flow (#2380)

* chore: fix off-center close button in level up notifications (#2382)

* chore: fix discrepency between original message and centered message border radius (#2383)

* chore: don't show presence indicator on small avatars (#2386)

* chore: give max height to image in activity suggestion dialog (#2403)

* chore: when navigating to space details, always open space view (#2405)

* chore: fix vertical alignment of tokens in HTML-formatted messages (#2406)

* added robot animation and message to instruct user to wait after too … (#2415)

* added robot animation and message to instruct user to wait after too many join with code attempts

* chore: formatting

* replaced hardcoded text with intl_en.arb

* Resolving missing import

* generated

* chore: formatting

---------

Co-authored-by: ggurdin <ggurdin@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* refactor: separate token and message reading assistance modes (#2416)

* refactor: separate token and message reading assistance modes

* chore: apply same token styling to HTML formatted messages

* chore: don't wait for lemma responses before showing reading assistance content

* 2421 reading assistance mode split feedback from will (#2422)

* chore: make input bar shorter in token mode

* chore: retry showing reading assistance content for initial token

* chore: make background lighter in token mode

* Added 'JoinByCode' button on new group view (#2417)

* Added 'JoinByCode' button on new group view

* generated

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com>

* fix(IT): added chreo code back + added original feedback star class back

* generated

* chore: revert change to it controller, use choreo record to determine which constructs are new

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ggurdin <46800240+ggurdin@users.noreply.github.com>
Co-authored-by: wcjord <32568597+wcjord@users.noreply.github.com>
Co-authored-by: Sofanyas Genene <123987957+Sofanyas@users.noreply.github.com>
Co-authored-by: ggurdin <ggurdin@gmail.com>
This commit is contained in:
sienna-sterling 2025-04-14 11:54:32 -04:00 committed by GitHub
parent d111b11783
commit ddbc215252
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 424 additions and 76 deletions

View file

@ -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<String> translations = [];
SimilartyResponseModel? similarityResponse;
AlternativeTranslator(this.choreographer);
void clear() {
@ -97,28 +102,86 @@ class AlternativeTranslator {
}
}
List<PangeaToken> 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<OneConstructUse> get _itStepConstructs {
final metadata = ConstructUseMetaData(
roomId: choreographer.roomId,
timeStamp: DateTime.now(),
);
int countVocabularyWordsFromSteps() =>
_selectedTokens.map((t) => t.lemma.text.toLowerCase()).toSet().length;
final List<OneConstructUse> 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<ConstructIdentifier, int> 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);

View file

@ -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<ITBar> 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),

View file

@ -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<TranslationFeedback> createState() => _TranslationFeedbackState();
}
class _TranslationFeedbackState extends State<TranslationFeedback>
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<double> _starsOpacity;
late Animation<double> _starsScale;
late Animation<double> _vocabOpacity;
late Animation<double> _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<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _starsController, curve: Curves.easeInOut),
);
_starsScale = Tween<double>(begin: 0.5, end: 1.0).animate(
CurvedAnimation(parent: _starsController, curve: Curves.elasticOut),
);
_vocabOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _vocabController, curve: Curves.easeInOut),
);
_grammarOpacity = Tween<double>(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<AnimatedCounter> createState() => _AnimatedCounterState();
}
class _AnimatedCounterState extends State<AnimatedCounter>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<int> _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,
);
},
);
}
}