1949-changing-it-feedback (#2088)

---------

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: ggurdin <ggurdin@gmail.com>
This commit is contained in:
sienna-sterling 2025-04-01 15:04:33 -04:00 committed by GitHub
parent 6e7ae5c044
commit 96197138ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 271 additions and 177 deletions

View file

@ -4849,5 +4849,11 @@
"wordFocusListeningMultipleChoice": "Which audio matches the word?",
"createActivity": "Create activity",
"startChat": "Start a chat",
"translationProblem": "Translation problem"
"translationProblem": "Translation problem",
"perfectTranslation": "Perfect translation!",
"greatJobTranslation": "Great job with this translation!",
"goodJobTranslation": "Good work on this translation.",
"makingProgress": "You're making progress!",
"keepPracticing": "Keep practicing!",
"niceJob": "Nice job!"
}

View file

@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_downloads/analytics_summary_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/learning_skills_enum.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
enum ConstructUseTypeEnum {
/// produced in chat by user, igc was run, and we've judged it to be a correct use

View file

@ -1,9 +1,10 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
class PointsGainedAnimation extends StatefulWidget {
final int points;

View file

@ -1,12 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_token_text.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import '../../../utils/matrix_sdk_extensions/matrix_locals.dart';
class ChatListItemSubtitle extends StatelessWidget {

View file

@ -3,10 +3,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:http/http.dart' as http;
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/choreographer/repo/full_text_translation_repo.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import '../repo/similarity_repo.dart';
class AlternativeTranslator {
@ -18,7 +19,6 @@ class AlternativeTranslator {
FeedbackKey? translationFeedbackKey;
List<String> translations = [];
SimilartyResponseModel? similarityResponse;
AlternativeTranslator(this.choreographer);
void clear() {
@ -31,71 +31,49 @@ class AlternativeTranslator {
similarityResponse = null;
}
double get _percentCorrectChoices {
final totalSteps = choreographer.choreoRecord.itSteps.length;
if (totalSteps == 0) return 0.0;
final int correctFirstAttempts = choreographer.itController.completedITSteps
.where(
(step) => !step.continuances.any(
(c) =>
c.level != ChoreoConstants.levelThresholdForGreen &&
c.wasClicked,
),
)
.length;
final double percentage = (correctFirstAttempts / totalSteps) * 100;
return percentage;
}
int get starRating {
final double percent = _percentCorrectChoices;
if (percent == 100) return 5;
if (percent >= 80) return 4;
if (percent >= 60) return 3;
if (percent >= 40) return 2;
if (percent > 0) return 1;
return 0;
}
Future<void> setTranslationFeedback() async {
try {
choreographer.startLoading();
translationFeedbackKey = FeedbackKey.loadingPleaseWait;
showTranslationFeedback = true;
userTranslation = choreographer.currentText;
if (choreographer.itController.allCorrect) {
final double percentCorrect = _percentCorrectChoices;
// Set feedback based on percentage
if (percentCorrect == 100) {
translationFeedbackKey = FeedbackKey.allCorrect;
return;
} else if (percentCorrect >= 80) {
translationFeedbackKey = FeedbackKey.newWayAllGood;
} else {
translationFeedbackKey = FeedbackKey.othersAreBetter;
}
final String? goldRouteTranslation =
choreographer.itController.goldRouteTracker.fullTranslation;
final FullTextTranslationResponseModel results =
await FullTextTranslationRepo.translate(
accessToken: choreographer.accessToken,
request: FullTextTranslationRequestModel(
text: choreographer.itController.sourceText!,
tgtLang: choreographer.l2LangCode!,
userL2: choreographer.l2LangCode!,
userL1: choreographer.l1LangCode!,
deepL: goldRouteTranslation == null,
),
);
translations = results.translations;
if (results.deepL != null || goldRouteTranslation != null) {
translations.insert(0, (results.deepL ?? goldRouteTranslation)!);
}
// final List<String> altAndUser = [...results.translations];
// if (results.deepL != null) {
// altAndUser.add(results.deepL!);
// }
// altAndUser.add(userTranslation);
if (userTranslation?.toLowerCase() ==
results.bestTranslation.toLowerCase()) {
translationFeedbackKey = FeedbackKey.allCorrect;
return;
}
similarityResponse = await SimilarityRepo.get(
accessToken: choreographer.accessToken,
request: SimilarityRequestModel(
benchmark: results.bestTranslation,
toCompare: [userTranslation!],
),
);
// if (similarityResponse!
// .userTranslationIsSameAsBotTranslation(userTranslation!)) {
// translationFeedbackKey = FeedbackKey.allCorrect;
// return;
// }
// if (similarityResponse!
// .userTranslationIsDifferentButBetter(userTranslation!)) {
// translationFeedbackKey = FeedbackKey.newWayAllGood;
// return;
// }
showAlternativeTranslations = true;
translationFeedbackKey = FeedbackKey.othersAreBetter;
} catch (err, stack) {
if (err is! http.Response) {
ErrorHandler.logError(
@ -119,36 +97,50 @@ class AlternativeTranslator {
}
}
String translationFeedback(BuildContext context) {
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();
int countVocabularyWordsFromSteps() =>
_selectedTokens.map((t) => t.lemma.text.toLowerCase()).toSet().length;
int countGrammarConstructsFromSteps() => _selectedTokens
.map(
(t) => t.morph.entries.map(
(m) => "${m.key}:${m.value}".toLowerCase(),
),
)
.expand((m) => m)
.toSet()
.length;
String getDefaultFeedback(BuildContext context) {
final l10n = L10n.of(context);
switch (translationFeedbackKey) {
case FeedbackKey.allCorrect:
return "Match: 100%\n${L10n.of(context).allCorrect}";
return l10n.perfectTranslation;
case FeedbackKey.newWayAllGood:
return "Match: 100%\n${L10n.of(context).newWayAllGood}";
return l10n.greatJobTranslation;
case FeedbackKey.othersAreBetter:
final num userScore =
(similarityResponse!.userScore(userTranslation!) * 100).round();
final String displayScore = userScore.toString();
if (userScore > 90) {
return "Match: $displayScore%\n${L10n.of(context).almostPerfect}";
if (_percentCorrectChoices >= 60) {
return l10n.goodJobTranslation;
}
if (userScore > 80) {
return "Match: $displayScore%\n${L10n.of(context).prettyGood}";
if (_percentCorrectChoices >= 40) {
return l10n.makingProgress;
}
return "Match: $displayScore%\n${L10n.of(context).othersAreBetter}";
// case FeedbackKey.commonalityFeedback:
// final int count = controller.completedITSteps
// .where((element) => element.isCorrect)
// .toList()
// .length;
// final int total = controller.completedITSteps.length;
// return L10n.of(context).commonalityFeedback(count,total);
return l10n.keepPracticing;
case FeedbackKey.loadingPleaseWait:
return L10n.of(context).letMeThink;
return l10n.letMeThink;
case FeedbackKey.allDone:
return L10n.of(context).allDone;
return l10n.allDone;
default:
return L10n.of(context).loadingPleaseWait;
return l10n.loadingPleaseWait;
}
}
}

View file

@ -230,6 +230,15 @@ class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
: itController.isTranslationDone
? TranslationFeedback(
controller: itController,
vocabCount: itController
.choreographer.altTranslator
.countVocabularyWordsFromSteps(),
grammarCount: itController
.choreographer.altTranslator
.countGrammarConstructsFromSteps(),
feedbackText: itController
.choreographer.altTranslator
.getDefaultFeedback(context),
)
: ITChoices(controller: itController),
),

View file

@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/choreographer/widgets/choice_animation.dart';
class FillingStars extends StatefulWidget {
final int rating;
const FillingStars({
super.key,
required this.rating,
});
@override
State<FillingStars> createState() => _FillingStarsState();
}
class _FillingStarsState extends State<FillingStars> {
final List<bool> _isFilledList = List.filled(5, false);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _animate());
}
Future<void> _animate() async {
for (int i = 0; i < widget.rating; i++) {
await Future.delayed(
const Duration(milliseconds: choiceArrayAnimationDuration), () {
if (mounted) {
setState(() => _isFilledList[i] = true);
}
});
}
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (index) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: choiceArrayAnimationDuration),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation,
child: child,
);
},
child: Icon(
_isFilledList[index] ? Icons.star_rounded : Icons.star_rounded,
key: ValueKey<bool>(_isFilledList[index]),
color: _isFilledList[index]
? AppConfig.gold
: Theme.of(context).cardColor.withAlpha(180),
size: 32.0,
),
);
}),
);
}
}

View file

@ -1,94 +1,99 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_stars.dart';
import '../../bot/utils/bot_style.dart';
import '../../common/utils/error_handler.dart';
import '../controllers/it_controller.dart';
import 'choice_array.dart';
class TranslationFeedback extends StatelessWidget {
final int vocabCount;
final int grammarCount;
final String feedbackText;
final ITController controller;
const TranslationFeedback({super.key, required this.controller});
const TranslationFeedback({
super.key,
required this.controller,
required this.vocabCount,
required this.grammarCount,
required this.feedbackText,
});
@override
Widget build(BuildContext context) {
String feedbackText;
TextStyle? style;
final altTranslator = controller.choreographer.altTranslator;
try {
feedbackText =
controller.choreographer.altTranslator.translationFeedback(context);
style = BotStyle.text(context);
return Column(
spacing: 16.0,
children: [
FillingStars(rating: altTranslator.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,
),
),
],
),
],
)
else
Text(
feedbackText,
textAlign: TextAlign.center,
style: BotStyle.text(context),
),
const SizedBox(height: 16.0),
],
);
} catch (err, stack) {
feedbackText = "Nice job!";
style = null;
debugPrint("error getting copy and styles");
debugPrint("Error in TranslationFeedback: $err");
ErrorHandler.logError(
e: err,
s: stack,
data: {
"feedbackText": controller.choreographer.altTranslator
.translationFeedback(context),
},
data: {},
);
// Fallback to a simple message if anything goes wrong
return Center(child: Text(L10n.of(context).niceJob));
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
if (controller.choreographer.altTranslator.showTranslationFeedback)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
children: [
TextSpan(
text: "$feedbackText ",
style: style,
),
],
),
),
),
const SizedBox(height: 6),
if (controller
.choreographer.altTranslator.showAlternativeTranslations)
AlternativeTranslations(controller: controller),
// if (!controller
// .choreographer.altTranslator.showAlternativeTranslations &&
// !controller.choreographer.isFetching)
// ITRestartButton(controller: controller),
],
),
);
}
}
class AlternativeTranslations extends StatelessWidget {
const AlternativeTranslations({
super.key,
required this.controller,
});
final ITController controller;
@override
Widget build(BuildContext context) {
return ChoicesArray(
originalSpan: controller.sourceText ?? "dummy",
isLoading:
controller.choreographer.altTranslator.loadingAlternativeTranslations,
// choices: controller.choreographer.altTranslator.similarityResponse.scores
choices: [
Choice(text: controller.choreographer.altTranslator.translations.first),
],
// choices: controller.choreographer.altTranslator.translations,
onPressed: (String value, int index) {
controller.choreographer.onSelectAlternativeTranslation(
controller.choreographer.altTranslator.translations[index],
);
},
selectedChoiceIndex: null,
tts: null,
);
}
}

View file

@ -1,9 +1,11 @@
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
class CustomizedSvg extends StatefulWidget {
/// URL of the SVG file
final String svgUrl;

View file

@ -1,6 +1,12 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
@ -17,10 +23,6 @@ import 'package:fluffychat/pangea/practice_activities/practice_record.dart';
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
import 'package:fluffychat/pangea/practice_activities/relevant_span_display_details.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class PracticeActivityModel {
List<PangeaToken> targetTokens;

View file

@ -1,6 +1,9 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart';
@ -9,7 +12,6 @@ import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.dart';
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
class PracticeSelection {
late String _userL2;

View file

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_selection.dart';
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
class PracticeSelectionRepo {
static final GetStorage _storage = GetStorage('practice_selection_cache');

View file

@ -1,5 +1,8 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/choreographer/widgets/choice_animation.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
@ -9,8 +12,6 @@ import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/practice_match_item.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class MatchActivityCard extends StatelessWidget {
final PracticeActivityModel currentActivity;

View file

@ -1,12 +1,13 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class PracticeMatchItem extends StatefulWidget {
const PracticeMatchItem({

View file

@ -1,4 +1,8 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
@ -11,8 +15,6 @@ import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
/// Question - does this need to be stateful or does this work?
/// Need to test.

View file

@ -1,3 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
@ -9,8 +13,6 @@ import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_audio_button.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class LemmaWidget extends StatefulWidget {
final PangeaToken token;