diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index ee9fb9f3a..cdb80ea99 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4530,7 +4530,8 @@ "updateNow": "Update Now", "updateLater": "Later", "constructUseWaDesc": "Used without help", - "constructUseGaDesc": "Grammar mistake", + "constructUseGaDesc": "Grammar assistance", + "constructUseTaDesc": "Translation assistance", "constructUseUnkDesc": "Unknown", "constructUseCorITDesc": "Correct in translation", "constructUseIgnITDesc": "Ignored in translation", @@ -4865,5 +4866,7 @@ "enterSpaceCode": "Enter the Space Code", "shareSpaceLink": "Share link to space", "byUsingPangeaChat": "By using Pangea Chat, I agree to the ", - "details": "Details" + "details": "Details", + "newVocab": "New vocab", + "newGrammar": "New grammar concepts" } \ No newline at end of file diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index 6499b8dfe..f333e1a49 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -5191,7 +5191,6 @@ "enterCodeToJoin": "Ingrese el código para unirse", "updateLater": "Más tarde", "constructUseWaDesc": "Usado sin ayuda", - "constructUseGaDesc": "Error gramatical", "constructUseUnkDesc": "Desconocido", "constructUseCorITDesc": "Correcto en la traducción", "constructUseIgnITDesc": "Ignorado en la traducción", diff --git a/assets/l10n/intl_vi.arb b/assets/l10n/intl_vi.arb index 27fc0c81a..ab30ec613 100644 --- a/assets/l10n/intl_vi.arb +++ b/assets/l10n/intl_vi.arb @@ -3377,7 +3377,6 @@ "updateNow": "Cập nhật ngay", "updateLater": "Để sau", "constructUseWaDesc": "Dùng không cần trợ giúp", - "constructUseGaDesc": "Có lỗi ngữ pháp", "constructUseUnkDesc": "Không xác định", "constructUseCorITDesc": "Đúng trong dịch", "constructUseIgnITDesc": "Bỏ qua trong dịch", diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index d1fa4d148..d97a9c501 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -27,6 +27,7 @@ import 'package:fluffychat/pages/chat/chat_view.dart'; import 'package:fluffychat/pages/chat/event_info_dialog.dart'; import 'package:fluffychat/pages/chat/recording_dialog.dart'; import 'package:fluffychat/pages/chat_details/chat_details.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/gain_points_animation.dart'; import 'package:fluffychat/pangea/analytics_misc/level_up.dart'; @@ -36,6 +37,7 @@ import 'package:fluffychat/pangea/chat/widgets/event_too_large_dialog.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart'; import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/igc/message_analytics_feedback.dart'; import 'package:fluffychat/pangea/choreographer/widgets/igc/pangea_text_controller.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; @@ -819,18 +821,46 @@ class ChatController extends State ); if (msgEventId != null && originalSent != null && tokensSent != null) { + final List constructs = [ + ...originalSent.vocabAndMorphUses( + choreo: choreo, + tokens: tokensSent.tokens, + metadata: metadata, + ), + ]; + + final newGrammarConstructs = + pangeaController.getAnalytics.newConstructCount( + constructs, + ConstructTypeEnum.morph, + ); + + final newVocabConstructs = + pangeaController.getAnalytics.newConstructCount( + constructs, + ConstructTypeEnum.vocab, + ); + + OverlayUtil.showOverlay( + overlayKey: "msg_analytics_feedback_$msgEventId", + followerAnchor: Alignment.bottomRight, + targetAnchor: Alignment.topRight, + context: context, + child: MessageAnalyticsFeedback( + overlayId: "msg_analytics_feedback_$msgEventId", + newGrammarConstructs: newGrammarConstructs, + newVocabConstructs: newVocabConstructs, + ), + transformTargetId: msgEventId, + ignorePointer: true, + ); + pangeaController.putAnalytics.setState( AnalyticsStream( eventId: msgEventId, targetID: msgEventId, roomId: room.id, - constructs: [ - ...originalSent.vocabAndMorphUses( - choreo: choreo, - tokens: tokensSent.tokens, - metadata: metadata, - ), - ], + constructs: constructs, ), ); } diff --git a/lib/pangea/analytics_details_popup/lemma_usage_dots.dart b/lib/pangea/analytics_details_popup/lemma_usage_dots.dart index 817fe763e..15911a298 100644 --- a/lib/pangea/analytics_details_popup/lemma_usage_dots.dart +++ b/lib/pangea/analytics_details_popup/lemma_usage_dots.dart @@ -26,13 +26,13 @@ class LemmaUsageDots extends StatelessWidget { List sortedUses(LearningSkillsEnum category) { final List useList = []; for (final OneConstructUse use in construct.uses) { - if (use.useType.pointValue == 0) { + if (use.xp == 0) { continue; } // If the use type matches the given category, save to list // Usage with positive XP is saved as true, else false if (category == use.useType.skillsEnumType) { - useList.add(use.useType.pointValue > 0); + useList.add(use.xp > 0); } } return useList; diff --git a/lib/pangea/analytics_details_popup/lemma_use_example_messages.dart b/lib/pangea/analytics_details_popup/lemma_use_example_messages.dart index bcce93b4e..5c3c9b4dc 100644 --- a/lib/pangea/analytics_details_popup/lemma_use_example_messages.dart +++ b/lib/pangea/analytics_details_popup/lemma_use_example_messages.dart @@ -29,7 +29,7 @@ class LemmaUseExampleMessages extends StatelessWidget { if (use.useType.skillsEnumType != LearningSkillsEnum.writing || use.metadata.eventId == null || use.form == null || - use.pointValue <= 0) { + use.xp <= 0) { continue; } diff --git a/lib/pangea/analytics_misc/analytics_summary_model.dart b/lib/pangea/analytics_misc/analytics_summary_model.dart index be7c57983..8ebfdf46d 100644 --- a/lib/pangea/analytics_misc/analytics_summary_model.dart +++ b/lib/pangea/analytics_misc/analytics_summary_model.dart @@ -124,7 +124,7 @@ class AnalyticsSummaryModel { use.useType != ConstructUseTypeEnum.wa && use.useType != ConstructUseTypeEnum.ga && use.useType != ConstructUseTypeEnum.unk && - use.pointValue != 0, + use.xp != 0, percent: 0.8, context: context, ); diff --git a/lib/pangea/analytics_misc/construct_list_model.dart b/lib/pangea/analytics_misc/construct_list_model.dart index 07323b499..edacf7741 100644 --- a/lib/pangea/analytics_misc/construct_list_model.dart +++ b/lib/pangea/analytics_misc/construct_list_model.dart @@ -354,7 +354,7 @@ class LemmasToUsesWrapper { final uses = entry.value.toList(); for (final use in uses) { - use.pointValue > 0 ? correctUses.add(use) : incorrectUses.add(use); + use.xp > 0 ? correctUses.add(use) : incorrectUses.add(use); } final totalUses = correctUses.length + incorrectUses.length; diff --git a/lib/pangea/analytics_misc/construct_use_model.dart b/lib/pangea/analytics_misc/construct_use_model.dart index c244f87ef..e03d4c5f9 100644 --- a/lib/pangea/analytics_misc/construct_use_model.dart +++ b/lib/pangea/analytics_misc/construct_use_model.dart @@ -24,7 +24,7 @@ class ConstructUses { int get points { return uses.fold( 0, - (total, use) => total + use.useType.pointValue, + (total, use) => total + use.xp, ); } @@ -46,8 +46,8 @@ class ConstructUses { return _category!.toLowerCase(); } - bool get hasCorrectUse => uses.any((use) => use.pointValue > 0); - bool get hasIncorrectUse => uses.any((use) => use.pointValue < 0); + bool get hasCorrectUse => uses.any((use) => use.xp > 0); + bool get hasIncorrectUse => uses.any((use) => use.xp < 0); ConstructIdentifier get id => ConstructIdentifier( lemma: lemma, diff --git a/lib/pangea/analytics_misc/construct_use_type_enum.dart b/lib/pangea/analytics_misc/construct_use_type_enum.dart index cad3d9047..d0c2eec3b 100644 --- a/lib/pangea/analytics_misc/construct_use_type_enum.dart +++ b/lib/pangea/analytics_misc/construct_use_type_enum.dart @@ -10,10 +10,12 @@ enum ConstructUseTypeEnum { /// produced in chat by user, igc was run, and we've judged it to be a correct use wa, - /// produced in chat by user, igc was run, and we've judged it to be a incorrect use - /// Note: if the IGC match is ignored, this is not counted as an incorrect use + /// produced during IGC ga, + /// produced during IT + ta, + /// produced in chat by user and igc was not run unk, @@ -78,6 +80,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { return L10n.of(context).constructUseWaDesc; case ConstructUseTypeEnum.ga: return L10n.of(context).constructUseGaDesc; + case ConstructUseTypeEnum.ta: + return L10n.of(context).constructUseTaDesc; case ConstructUseTypeEnum.unk: return L10n.of(context).constructUseUnkDesc; case ConstructUseTypeEnum.corIt: @@ -149,6 +153,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.incIGC: case ConstructUseTypeEnum.corIGC: case ConstructUseTypeEnum.ga: + case ConstructUseTypeEnum.ta: return Icons.spellcheck; case ConstructUseTypeEnum.corPA: case ConstructUseTypeEnum.incPA: @@ -223,9 +228,10 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.ignMM: case ConstructUseTypeEnum.unk: case ConstructUseTypeEnum.nan: + case ConstructUseTypeEnum.ga: + case ConstructUseTypeEnum.ta: return 0; - case ConstructUseTypeEnum.ga: case ConstructUseTypeEnum.incMM: case ConstructUseTypeEnum.incIt: case ConstructUseTypeEnum.incIGC: @@ -244,6 +250,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { switch (this) { case ConstructUseTypeEnum.wa: case ConstructUseTypeEnum.ga: + case ConstructUseTypeEnum.ta: case ConstructUseTypeEnum.unk: case ConstructUseTypeEnum.corIt: case ConstructUseTypeEnum.ignIt: @@ -283,6 +290,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { switch (this) { case ConstructUseTypeEnum.wa: case ConstructUseTypeEnum.ga: + case ConstructUseTypeEnum.ta: case ConstructUseTypeEnum.unk: case ConstructUseTypeEnum.corIt: case ConstructUseTypeEnum.ignIt: @@ -323,6 +331,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { switch (this) { case ConstructUseTypeEnum.wa: case ConstructUseTypeEnum.ga: + case ConstructUseTypeEnum.ta: case ConstructUseTypeEnum.unk: case ConstructUseTypeEnum.pvm: return AnalyticsSummaryEnum.numWordsTyped; diff --git a/lib/pangea/analytics_misc/constructs_model.dart b/lib/pangea/analytics_misc/constructs_model.dart index ae10dc79c..c252202c5 100644 --- a/lib/pangea/analytics_misc/constructs_model.dart +++ b/lib/pangea/analytics_misc/constructs_model.dart @@ -90,6 +90,8 @@ class OneConstructUse { String? id; ConstructUseMetaData metadata; + int xp; + OneConstructUse({ required this.useType, required this.lemma, @@ -97,6 +99,7 @@ class OneConstructUse { required this.metadata, required category, required this.form, + required this.xp, this.id, }) { if (category is MorphFeaturesEnum) { @@ -117,8 +120,10 @@ class OneConstructUse { ? ConstructTypeUtil.fromString(json['constructType']) : ConstructTypeEnum.vocab; + final useType = ConstructUseTypeUtil.fromString(json['useType']); + return OneConstructUse( - useType: ConstructUseTypeUtil.fromString(json['useType']), + useType: useType, lemma: json['lemma'], form: json['form'], category: getCategory(json, constructType), @@ -129,6 +134,7 @@ class OneConstructUse { roomId: json['chatId'], timeStamp: DateTime.parse(json['timeStamp']), ), + xp: json['xp'] ?? useType.pointValue, ); } @@ -142,6 +148,7 @@ class OneConstructUse { 'constructType': constructType.string, 'categories': category, 'id': id, + 'xp': xp, }; String get category { @@ -196,11 +203,9 @@ class OneConstructUse { return room.getEventById(metadata.eventId!); } - int get pointValue => useType.pointValue; - Color pointValueColor(BuildContext context) { - if (pointValue == 0) return Theme.of(context).colorScheme.primary; - return pointValue > 0 ? AppConfig.gold : Colors.red; + if (xp == 0) return Theme.of(context).colorScheme.primary; + return xp > 0 ? AppConfig.gold : Colors.red; } ConstructIdentifier get identifier => ConstructIdentifier( diff --git a/lib/pangea/analytics_misc/get_analytics_controller.dart b/lib/pangea/analytics_misc/get_analytics_controller.dart index e6526582e..8d6a26b68 100644 --- a/lib/pangea/analytics_misc/get_analytics_controller.dart +++ b/lib/pangea/analytics_misc/get_analytics_controller.dart @@ -171,7 +171,7 @@ class GetAnalyticsController extends BaseController { _updateAnalyticsStream( points: analyticsUpdate.newConstructs.fold( 0, - (previousValue, element) => previousValue + element.pointValue, + (previousValue, element) => previousValue + element.xp, ), targetID: analyticsUpdate.targetID, newConstructs: [...newUnlockedMorphs, ...newUnlockedVocab], @@ -398,6 +398,29 @@ class GetAnalyticsController extends BaseController { _cache.add(entry); } + int newConstructCount( + List newConstructs, + ConstructTypeEnum type, + ) { + final uses = newConstructs.where((c) => c.constructType == type); + final Map constructPoints = {}; + for (final use in uses) { + constructPoints[use.identifier] ??= 0; + constructPoints[use.identifier] = + constructPoints[use.identifier]! + use.xp; + } + + int newConstructCount = 0; + for (final entry in constructPoints.entries) { + final construct = constructListModel.getConstructUses(entry.key); + if (construct == null || construct.points == entry.value) { + newConstructCount++; + } + } + + return newConstructCount; + } + // Future // _generateLevelUpAnalyticsAndSaveToStateEvent( // final int lowerLevel, diff --git a/lib/pangea/analytics_misc/put_analytics_controller.dart b/lib/pangea/analytics_misc/put_analytics_controller.dart index 7a7110e64..ae17b567c 100644 --- a/lib/pangea/analytics_misc/put_analytics_controller.dart +++ b/lib/pangea/analytics_misc/put_analytics_controller.dart @@ -12,7 +12,6 @@ import 'package:fluffychat/pangea/common/constants/local.key.dart'; import 'package:fluffychat/pangea/common/controllers/base_controller.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -122,17 +121,17 @@ class PutAnalyticsController extends BaseController { final String? eventID = data.eventId; final String? roomID = data.roomId; - List constructs = []; - if (roomID != null) { - constructs = _getDraftUses(roomID); - } + final List constructs = []; + // if (roomID != null) { + // constructs = _getDraftUses(roomID); + // } constructs.addAll(data.constructs); if (kDebugMode) { for (final use in constructs) { debugPrint( - "_onNewAnalyticsData filtered use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}", + "_onNewAnalyticsData filtered use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.xp}", ); } } @@ -161,87 +160,87 @@ class PutAnalyticsController extends BaseController { }); } - void addDraftUses( - List tokens, - String roomID, - ConstructUseTypeEnum useType, { - String? targetID, - }) { - final metadata = ConstructUseMetaData( - roomId: roomID, - timeStamp: DateTime.now(), - ); - - // we only save those with saveVocab == true - final tokensToSave = - tokens.where((token) => token.lemma.saveVocab).toList(); - - // get all our vocab constructs - final uses = tokensToSave - .map( - (token) => OneConstructUse( - useType: useType, - lemma: token.lemma.text, - form: token.text.content, - constructType: ConstructTypeEnum.vocab, - metadata: metadata, - category: token.pos, - ), - ) - .toList(); - - // get all our grammar constructs - for (final token in tokensToSave) { - uses.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) { - uses.add( - OneConstructUse( - useType: useType, - lemma: entry.value, - form: token.text.content, - category: entry.key, - constructType: ConstructTypeEnum.morph, - metadata: metadata, - ), - ); - } - } - - if (kDebugMode) { - for (final use in uses) { - debugPrint( - "Draft use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}", - ); - } - } - - final level = _pangeaController.getAnalytics.constructListModel.level; - - // the list 'uses' gets altered in the _addLocalMessage method, - // so copy it here to that the list of new uses is accurate - final List newUses = List.from(uses); - _addLocalMessage('draft$roomID', uses).then( - (_) => _decideWhetherToUpdateAnalyticsRoom( - level, - targetID, - newUses, - ), - ); - } - - List _getDraftUses(String roomID) { - final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate; - return currentCache['draft$roomID'] ?? []; - } + // void addDraftUses( + // List tokens, + // String roomID, + // ConstructUseTypeEnum useType, { + // String? targetID, + // }) { + // final metadata = ConstructUseMetaData( + // roomId: roomID, + // timeStamp: DateTime.now(), + // ); + + // // we only save those with saveVocab == true + // final tokensToSave = + // tokens.where((token) => token.lemma.saveVocab).toList(); + + // // get all our vocab constructs + // final uses = tokensToSave + // .map( + // (token) => OneConstructUse( + // useType: useType, + // lemma: token.lemma.text, + // form: token.text.content, + // constructType: ConstructTypeEnum.vocab, + // metadata: metadata, + // category: token.pos, + // ), + // ) + // .toList(); + + // // get all our grammar constructs + // for (final token in tokensToSave) { + // uses.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) { + // uses.add( + // OneConstructUse( + // useType: useType, + // lemma: entry.value, + // form: token.text.content, + // category: entry.key, + // constructType: ConstructTypeEnum.morph, + // metadata: metadata, + // ), + // ); + // } + // } + + // if (kDebugMode) { + // for (final use in uses) { + // debugPrint( + // "Draft use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}", + // ); + // } + // } + + // final level = _pangeaController.getAnalytics.constructListModel.level; + + // // the list 'uses' gets altered in the _addLocalMessage method, + // // so copy it here to that the list of new uses is accurate + // final List newUses = List.from(uses); + // _addLocalMessage('draft$roomID', uses).then( + // (_) => _decideWhetherToUpdateAnalyticsRoom( + // level, + // targetID, + // newUses, + // ), + // ); + // } + + // List _getDraftUses(String roomID) { + // final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate; + // return currentCache['draft$roomID'] ?? []; + // } void _clearDraftUses(String roomID) { final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate; diff --git a/lib/pangea/analytics_summary/level_bar_popup.dart b/lib/pangea/analytics_summary/level_bar_popup.dart index 09eae2494..700b9fee8 100644 --- a/lib/pangea/analytics_summary/level_bar_popup.dart +++ b/lib/pangea/analytics_summary/level_bar_popup.dart @@ -155,7 +155,7 @@ class LevelBarPopup extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - "${use.pointValue > 0 ? '+' : ''}${use.pointValue}", + "${use.xp > 0 ? '+' : ''}${use.xp}", style: TextStyle( fontWeight: FontWeight.w900, fontSize: 14, diff --git a/lib/pangea/choreographer/controllers/alternative_translator.dart b/lib/pangea/choreographer/controllers/alternative_translator.dart deleted file mode 100644 index 550acef25..000000000 --- a/lib/pangea/choreographer/controllers/alternative_translator.dart +++ /dev/null @@ -1,219 +0,0 @@ -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/constructs/construct_identifier.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import '../repo/similarity_repo.dart'; - -class AlternativeTranslator { - final Choreographer choreographer; - bool showAlternativeTranslations = false; - bool loadingAlternativeTranslations = false; - bool showTranslationFeedback = false; - String? userTranslation; - FeedbackKey? translationFeedbackKey; - List translations = []; - SimilartyResponseModel? similarityResponse; - - AlternativeTranslator(this.choreographer); - - void clear() { - userTranslation = null; - showAlternativeTranslations = false; - loadingAlternativeTranslations = false; - showTranslationFeedback = false; - translationFeedbackKey = null; - translations = []; - 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 setTranslationFeedback() async { - try { - choreographer.startLoading(); - translationFeedbackKey = FeedbackKey.loadingPleaseWait; - showTranslationFeedback = true; - userTranslation = choreographer.currentText; - - final double percentCorrect = _percentCorrectChoices; - - // Set feedback based on percentage - if (percentCorrect == 100) { - translationFeedbackKey = FeedbackKey.allCorrect; - } else if (percentCorrect >= 80) { - translationFeedbackKey = FeedbackKey.newWayAllGood; - } else { - translationFeedbackKey = FeedbackKey.othersAreBetter; - } - } catch (err, stack) { - if (err is! http.Response) { - ErrorHandler.logError( - e: err, - s: stack, - data: { - "sourceText": choreographer.itController.sourceText, - "currentText": choreographer.currentText, - "userL1": choreographer.l1LangCode, - "userL2": choreographer.l2LangCode, - "goldRouteTranslation": - choreographer.itController.goldRouteTracker.fullTranslation, - }, - ); - } - choreographer.errorService.setError( - ChoreoError(type: ChoreoErrorType.unknown, raw: err), - ); - } finally { - choreographer.stopLoading(); - } - } - - List get _itStepConstructs { - final metadata = ConstructUseMetaData( - roomId: choreographer.roomId, - timeStamp: DateTime.now(), - ); - - 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; - - 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); - switch (translationFeedbackKey) { - case FeedbackKey.allCorrect: - return l10n.perfectTranslation; - case FeedbackKey.newWayAllGood: - return l10n.greatJobTranslation; - case FeedbackKey.othersAreBetter: - if (_percentCorrectChoices >= 60) { - return l10n.goodJobTranslation; - } - if (_percentCorrectChoices >= 40) { - return l10n.makingProgress; - } - return l10n.keepPracticing; - case FeedbackKey.loadingPleaseWait: - return l10n.letMeThink; - case FeedbackKey.allDone: - return l10n.allDone; - default: - return l10n.loadingPleaseWait; - } - } -} - -enum FeedbackKey { - allCorrect, - newWayAllGood, - othersAreBetter, - loadingPleaseWait, - allDone, -} - -extension FeedbackKeyExtension on FeedbackKey {} diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 6495da1d0..55b001619 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -7,13 +7,13 @@ import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:fluffychat/pages/chat/chat.dart'; -import 'package:fluffychat/pangea/choreographer/controllers/alternative_translator.dart'; import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.dart'; import 'package:fluffychat/pangea/choreographer/enums/assistance_state_enum.dart'; import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart'; import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart'; import 'package:fluffychat/pangea/choreographer/models/it_step.dart'; import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; +import 'package:fluffychat/pangea/choreographer/repo/language_detection_repo.dart'; import 'package:fluffychat/pangea/choreographer/utils/input_paste_listener.dart'; import 'package:fluffychat/pangea/choreographer/widgets/igc/pangea_text_controller.dart'; import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart'; @@ -24,6 +24,7 @@ import 'package:fluffychat/pangea/common/utils/overlay.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/representation_content_model.dart'; import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'; +import 'package:fluffychat/pangea/events/repo/token_api_models.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/learning_settings/models/language_model.dart'; import 'package:fluffychat/pangea/spaces/models/space_model.dart'; @@ -41,7 +42,6 @@ class Choreographer { late PangeaTextController _textController; late ITController itController; late IgcController igc; - late AlternativeTranslator altTranslator; late ErrorService errorService; late TtsController tts; @@ -71,7 +71,6 @@ class Choreographer { itController = ITController(this); igc = IgcController(this); errorService = ErrorService(this); - altTranslator = AlternativeTranslator(this); _textController.addListener(_onChangeListener); _trialStream = pangeaController .subscriptionController.trialActivationStream.stream @@ -144,6 +143,7 @@ class Choreographer { return; } + chatController.sendFakeMessage(); final PangeaRepresentation? originalWritten = choreoRecord.includedIT && itController.sourceText != null ? PangeaRepresentation( @@ -154,59 +154,43 @@ class Choreographer { ) : null; - // we've got a rather elaborate method of updating tokens after matches are accepted - // so we need to check if the reconstructed text matches the current text - // if not, let's get the tokens again and log an error - if (igc.igcTextData?.tokens != null && - PangeaToken.reconstructText(igc.igcTextData!.tokens).trim() != - currentText.trim()) { - if (kDebugMode) { - PangeaToken.reconstructText( - igc.igcTextData!.tokens, - debugWalkThrough: true, - ); - } - ErrorHandler.logError( - m: "reconstructed text not working", - s: StackTrace.current, - data: { - "igcTextData": igc.igcTextData?.toJson(), - "choreoRecord": choreoRecord.toJson(), - }, - ); - - await igc.getIGCTextData(onlyTokensAndLanguageDetection: true); - } - - // TODO - why does both it and igc need to be enabled for choreo to be applicable? - // final ChoreoRecord? applicableChoreo = - // isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null; - - // if tokens OR language detection are not available, we should get them - // notes - // 1) we probably need to move this to after we clear the input field - // or the user could experience some lag here. - // 2) that this call is being made after we've determined if we have an applicable choreo in order to - // say whether correction was run on the message. we may eventually want - // to edit the useType after - if ((l2Lang != null && l1Lang != null) && - (igc.igcTextData?.tokens == null || - igc.igcTextData?.detectedLanguage == null)) { - await igc.getIGCTextData(onlyTokensAndLanguageDetection: true); - } + final detectionResp = await LanguageDetectionRepo.get( + MatrixState.pangeaController.userController.accessToken, + request: LanguageDetectionRequest( + text: currentText, + senderl1: l1LangCode, + senderl2: l2LangCode, + ), + ); + final detections = detectionResp.detections; + final detectedLanguage = + detections.firstOrNull?.langCode ?? LanguageKeys.unknownLanguage; final PangeaRepresentation originalSent = PangeaRepresentation( - langCode: - igc.igcTextData?.detectedLanguage ?? LanguageKeys.unknownLanguage, + langCode: detectedLanguage, text: currentText, originalSent: true, originalWritten: originalWritten == null, ); - final PangeaMessageTokens? tokensSent = igc.igcTextData?.tokens != null + List? res; + if (l1LangCode != null && l2LangCode != null) { + res = await pangeaController.messageData.getTokens( + repEventId: null, + room: chatController.room, + req: TokensRequestModel( + fullText: currentText, + langCode: detectedLanguage, + senderL1: l1LangCode!, + senderL2: l2LangCode!, + ), + ); + } + + final PangeaMessageTokens? tokensSent = res != null ? PangeaMessageTokens( - tokens: igc.igcTextData!.tokens, - detections: igc.igcTextData!.detections.detections, + tokens: res, + detections: detections, ) : null; @@ -235,7 +219,7 @@ class Choreographer { } choreoMode = ChoreoMode.it; itController.initializeIT( - ITStartData(_textController.text, igc.igcTextData?.detectedLanguage), + ITStartData(_textController.text, null), ); itMatch.status = PangeaMatchStatus.accepted; @@ -284,9 +268,7 @@ class Choreographer { () => getLanguageHelp(), ); } else { - getLanguageHelp( - onlyTokensAndLanguageDetection: ChoreoMode.it == choreoMode, - ); + getLanguageHelp(); } //Note: we don't set the keyboard type on each keyboard stroke so this is how we default to @@ -300,7 +282,6 @@ class Choreographer { /// or if autoIGC is not enabled and the user has not manually requested it. /// [onlyTokensAndLanguageDetection] will Future getLanguageHelp({ - bool onlyTokensAndLanguageDetection = false, bool manual = false, }) async { try { @@ -320,17 +301,13 @@ class Choreographer { // if getting language assistance after finishing IT, // reset the itController - if (choreoMode == ChoreoMode.it && - itController.isTranslationDone && - !onlyTokensAndLanguageDetection) { + if (choreoMode == ChoreoMode.it && itController.isTranslationDone) { itController.clear(); } await (isRunningIT ? itController.getTranslationData(_useCustomInput) - : igc.getIGCTextData( - onlyTokensAndLanguageDetection: onlyTokensAndLanguageDetection, - )); + : igc.getIGCTextData()); } catch (err, stack) { ErrorHandler.logError( e: err, @@ -343,7 +320,6 @@ class Choreographer { "itEnabled": itEnabled, "isAutoIGCEnabled": isAutoIGCEnabled, "isTranslationDone": itController.isTranslationDone, - "onlyTokensAndLanguageDetection": onlyTokensAndLanguageDetection, "useCustomInput": _useCustomInput, }, ); @@ -365,18 +341,6 @@ class Choreographer { giveInputFocus(); } - void onPredictorSelect(String text) { - //TODO - add some kind of record of this - // choreoRecord.addRecord(_textController.text, step: step); - - // TODO - probably give it a different type of edit type - _textController.setSystemText( - "${_textController.text} $text", - EditType.other, - ); - giveInputFocus(); - } - Future onReplacementSelect({ required int matchIndex, required int choiceIndex, @@ -546,21 +510,6 @@ class Choreographer { } } - void onSelectAlternativeTranslation(String translation) { - // PTODO - add some kind of record of this - // choreoRecord.addRecord(_textController.text, match); - - _textController.setSystemText( - translation, - EditType.alternativeTranslation, - ); - altTranslator.clear(); - altTranslator.translationFeedbackKey = FeedbackKey.allDone; - altTranslator.showTranslationFeedback = true; - giveInputFocus(); - setState(); - } - void giveInputFocus() { Future.delayed(Duration.zero, () { chatController.inputFocus.requestFocus(); @@ -586,22 +535,6 @@ class Choreographer { _resetDebounceTimer(); } - void onMatchError({int? cursorOffset}) { - if (cursorOffset == null) { - igc.igcTextData?.matches.clear(); - } else { - final int? matchIndex = igc.igcTextData?.getTopMatchIndexForOffset( - cursorOffset, - ); - matchIndex == -1 || matchIndex == null - ? igc.igcTextData?.matches.clear() - : igc.igcTextData?.matches.removeAt(matchIndex); - } - - setState(); - giveInputFocus(); - } - Future onPaste(value) async { choreoRecord.pastedStrings.add(value); } @@ -698,33 +631,12 @@ class Choreographer { chatController.room, ); - // bool get itAutoPlayEnabled { - // return pangeaController.userController.profile.userSettings.itAutoPlay; - // } - - bool get definitionsEnabled => - pangeaController.permissionsController.isToolEnabled( - ToolSetting.definitions, - chatController.room, - ); - bool get immersionMode => pangeaController.permissionsController.isToolEnabled( ToolSetting.immersionMode, chatController.room, ); - // bool get translationEnabled => - // pangeaController.permissionsController.isToolEnabled( - // ToolSetting.translations, - // chatController.room, - // ); - - bool get isITandIGCEnabled => - pangeaController.permissionsController.isWritingAssistanceEnabled( - chatController.room, - ); - bool get isAutoIGCEnabled => pangeaController.permissionsController.isToolEnabled( ToolSetting.autoIGC, diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 3e362a697..c917653ab 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -72,32 +72,21 @@ class IgcController { }); } - Future getIGCTextData({ - required bool onlyTokensAndLanguageDetection, - }) async { + Future getIGCTextData() async { try { if (choreographer.currentText.isEmpty) return clear(); - - // if tokenizing on message send, tokenization might take a while - // so add a fake event to the timeline to visually indicate that the message is being sent - if (onlyTokensAndLanguageDetection && - choreographer.choreoMode != ChoreoMode.it) { - choreographer.chatController.sendFakeMessage(); - } - debugPrint('getIGCTextData called with ${choreographer.currentText}'); - debugPrint( - 'getIGCTextData called with tokensOnly = $onlyTokensAndLanguageDetection', - ); final IGCRequestBody reqBody = IGCRequestBody( fullText: choreographer.currentText, userId: choreographer.pangeaController.userController.userId!, userL1: choreographer.l1LangCode!, userL2: choreographer.l2LangCode!, - enableIGC: choreographer.igcEnabled && !onlyTokensAndLanguageDetection, - enableIT: choreographer.itEnabled && !onlyTokensAndLanguageDetection, - prevMessages: prevMessages(), + enableIGC: choreographer.igcEnabled && + choreographer.choreoMode != ChoreoMode.it, + enableIT: choreographer.itEnabled && + choreographer.choreoMode != ChoreoMode.it, + prevMessages: _prevMessages(), ); if (_cacheClearTimer == null || !_cacheClearTimer!.isActive) { @@ -142,37 +131,7 @@ class IgcController { } } - // If there are any matches that don't match up with token offsets, - // this indicates and choreographer bug. Remove them. - final tokens = igcTextData!.tokens; - final List confirmedMatches = List.from(filteredMatches); - for (final match in filteredMatches) { - final substring = match.match.fullText.characters - .skip(match.match.offset) - .take(match.match.length); - final trimmed = substring.toString().trim().characters; - final matchOffset = (match.match.offset + match.match.length) - - (substring.length - trimmed.length); - final hasStartMatch = tokens.any( - (token) => token.text.offset == match.match.offset, - ); - final hasEndMatch = tokens.any( - (token) => token.text.offset + token.text.length == matchOffset, - ); - if (!hasStartMatch || !hasEndMatch) { - confirmedMatches.clear(); - ErrorHandler.logError( - m: "Match offset and/or length do not tokens with matching offset and length. This is a choreographer bug.", - data: { - "match": match.toJson(), - "tokens": tokens.map((e) => e.toJson()).toList(), - }, - ); - break; - } - } - - igcTextData!.matches = confirmedMatches; + igcTextData!.matches = filteredMatches; choreographer.acceptNormalizationMatches(); // TODO - for each new match, @@ -200,7 +159,6 @@ class IgcController { e: err, s: stack, data: { - "onlyTokensAndLanguageDetection": onlyTokensAndLanguageDetection, "currentText": choreographer.currentText, "userL1": choreographer.l1LangCode, "userL2": choreographer.l2LangCode, @@ -275,7 +233,7 @@ class IgcController { /// Get the content of previous text and audio messages in chat. /// Passed to IGC request to add context. - List prevMessages({int numMessages = 5}) { + List _prevMessages({int numMessages = 5}) { final List events = choreographer.chatController.visibleEvents .where( (e) => diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 77eae649e..93fa51623 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -2,17 +2,14 @@ import 'dart:async'; import 'dart:developer'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:sentry_flutter/sentry_flutter.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/error_service.dart'; import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../models/custom_input_translation_model.dart'; import '../models/it_response_model.dart'; @@ -56,7 +53,6 @@ class ITController { goldRouteTracker = GoldRouteTracker.defaultTracker; payLoadIds = []; - choreographer.altTranslator.clear(); choreographer.choreoMode = ChoreoMode.igc; choreographer.setState(); } @@ -164,11 +160,10 @@ class ITController { if (isTranslationDone) { nextITStep = null; - choreographer.altTranslator.setTranslationFeedback(); - choreographer.getLanguageHelp(onlyTokensAndLanguageDetection: true); + closeIT(); } else { nextITStep = Completer(); - final nextStep = await getNextTranslationData(); + final nextStep = await _getNextTranslationData(); nextITStep?.complete(nextStep); } } catch (e, s) { @@ -192,7 +187,7 @@ class ITController { } } - Future getNextTranslationData() async { + Future _getNextTranslationData() async { if (sourceText == null) { ErrorHandler.logError( e: Exception("sourceText is null in getNextTranslationData"), @@ -300,20 +295,6 @@ class ITController { ); } - // MessageServiceModel? messageServiceModelWithMessageId() => - // usedInteractiveTranslation - // ? MessageServiceModel( - // classId: choreographer.classId, - // roomId: choreographer.roomId, - // message: choreographer.currentText, - // messageId: null, - // payloadIds: payLoadIds, - // userId: choreographer.userId!, - // l1Lang: sourceLangCode, - // l2Lang: targetLangCode, - // ) - // : null; - //maybe we store IT data in the same format? make a specific kind of match? void selectTranslation(int chosenIndex) { if (currentITStep == null) return; @@ -323,20 +304,6 @@ class ITController { showChoiceFeedback = true; - // Get a list of the choices that the user did not click - final List? ignoredTokens = currentITStep?.continuances - .where((e) => !e.wasClicked) - .map((e) => e.tokens) - .expand((e) => e) - .toList(); - - // Save those choices' tokens to local construct analytics as ignored tokens - choreographer.pangeaController.putAnalytics.addDraftUses( - ignoredTokens ?? [], - choreographer.roomId, - ConstructUseTypeEnum.ignIt, - ); - Future.delayed( const Duration( milliseconds: ChoreoConstants.millisecondsToDisplayFeedback, @@ -357,8 +324,6 @@ class ITController { payLoadIds.add(res.payloadId); } - bool get usedInteractiveTranslation => sourceText != null; - bool get isTranslationDone => currentITStep != null && currentITStep!.isFinal; bool get isOpen => _isOpen; @@ -371,41 +336,12 @@ class ITController { bool get isLoading => choreographer.isFetching; - String latestChoiceFeedback(BuildContext context) => - completedITSteps.isNotEmpty - ? completedITSteps.last.choiceFeedback(context) - : ""; - - // String translationFeedback(BuildContext context) => - // completedITSteps.isNotEmpty - // ? completedITSteps.last.translationFeedback(context) - // : ""; - - bool get showAlternativeTranslationsOption => completedITSteps.isNotEmpty - ? completedITSteps.last.showAlternativeTranslationOption && - sourceText != null - : false; - - setIsEditingSourceText(bool value) { + void setIsEditingSourceText(bool value) { _isEditingSourceText = value; choreographer.setState(); } bool get isEditingSourceText => _isEditingSourceText; - - int get correctChoices => - completedITSteps.where((element) => element.isCorrect).length; - - int get wildcardChoices => - completedITSteps.where((element) => element.isYellow).length; - - int get incorrectChoices => - completedITSteps.where((element) => element.isWrong).length; - - int get customChoices => - completedITSteps.where((element) => element.isCustom).length; - - bool get allCorrect => completedITSteps.every((element) => element.isCorrect); } class ITStartData { diff --git a/lib/pangea/choreographer/models/choreo_record.dart b/lib/pangea/choreographer/models/choreo_record.dart index b6af64479..8adb25316 100644 --- a/lib/pangea/choreographer/models/choreo_record.dart +++ b/lib/pangea/choreographer/models/choreo_record.dart @@ -177,6 +177,17 @@ class ChoreoRecordStep { data[_stepKey] = itStep?.toJson(); return data; } + + List? get choices { + if (itStep != null) { + return itStep!.continuances.map((e) => e.text).toList().cast(); + } + + return acceptedOrIgnoredMatch?.match.choices + ?.map((e) => e.value) + .toList() + .cast(); + } } // Example flow diff --git a/lib/pangea/choreographer/models/igc_text_data_model.dart b/lib/pangea/choreographer/models/igc_text_data_model.dart index 05baa063e..806c72369 100644 --- a/lib/pangea/choreographer/models/igc_text_data_model.dart +++ b/lib/pangea/choreographer/models/igc_text_data_model.dart @@ -6,25 +6,19 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart'; -import 'package:fluffychat/pangea/choreographer/models/language_detection_model.dart'; import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/choreographer/models/span_data.dart'; -import 'package:fluffychat/pangea/choreographer/repo/language_detection_repo.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_representation_event.dart'; -import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/representation_content_model.dart'; -import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/widgets/matrix.dart'; // import 'package:language_tool/language_tool.dart'; class IGCTextData { - LanguageDetectionResponse detections; String originalInput; String? fullTextCorrection; - List tokens; List matches; String userL1; String userL2; @@ -33,10 +27,8 @@ class IGCTextData { bool loading = false; IGCTextData({ - required this.detections, required this.originalInput, required this.fullTextCorrection, - required this.tokens, required this.matches, required this.userL1, required this.userL2, @@ -45,25 +37,7 @@ class IGCTextData { }); factory IGCTextData.fromJson(Map json) { - // changing this to allow for use of the LanguageDetectionResponse methods - // TODO - change API after we're sure all clients are updated. not urgent. - final LanguageDetectionResponse detections = - json[_detectionsKey] is Iterable - ? LanguageDetectionResponse.fromJson({ - "detections": json[_detectionsKey], - "full_text": json["original_input"], - }) - : LanguageDetectionResponse.fromJson( - json[_detectionsKey] as Map, - ); - return IGCTextData( - tokens: (json[_tokensKey] as Iterable) - .map( - (e) => PangeaToken.fromJson(e as Map), - ) - .toList() - .cast(), matches: json[_matchesKey] != null ? (json[_matchesKey] as Iterable) .map( @@ -74,7 +48,6 @@ class IGCTextData { .toList() .cast() : [], - detections: detections, originalInput: json["original_input"], fullTextCorrection: json["full_text_correction"], userL1: json[ModelKey.userL1], @@ -90,7 +63,6 @@ class IGCTextData { String userL2, ) { final PangeaRepresentation content = event.content; - final List tokens = event.tokens ?? []; final List matches = event.choreo?.choreoSteps .map((step) => step.acceptedOrIgnoredMatch) .whereType() @@ -102,38 +74,9 @@ class IGCTextData { originalInput = matches.first.match.fullText; } - final defaultDetections = LanguageDetectionResponse( - detections: [ - LanguageDetection(langCode: content.langCode, confidence: 1), - ], - fullText: content.text, - ); - - LanguageDetectionResponse detections = defaultDetections; - if (event.detections != null) { - try { - detections = LanguageDetectionResponse.fromJson({ - "detections": event.detections, - "full_text": content.text, - }); - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - m: "Error parsing detections in IGCTextData.fromRepresentationEvent", - data: { - "detections": event.detections, - "full_text": content.text, - }, - ); - } - } - return IGCTextData( - detections: detections, originalInput: originalInput, fullTextCorrection: content.text, - tokens: tokens, matches: matches, userL1: userL1, userL2: userL2, @@ -142,15 +85,11 @@ class IGCTextData { ); } - static const String _tokensKey = "tokens"; static const String _matchesKey = "matches"; - static const String _detectionsKey = "detections"; Map toJson() => { - _detectionsKey: detections.toJson(), "original_input": originalInput, "full_text_correction": fullTextCorrection, - _tokensKey: tokens.map((e) => e.toJson()).toList(), _matchesKey: matches.map((e) => e.toJson()).toList(), ModelKey.userL1: userL1, ModelKey.userL2: userL2, @@ -158,15 +97,6 @@ class IGCTextData { "enable_igc": enableIGC, }; - /// if we haven't run IGC or IT or there are no matches, we use the highest validated detection - /// from [LanguageDetectionResponse.highestValidatedDetection] - /// if we have run igc/it and there are no matches, we can relax the threshold - /// and use the highest confidence detection - String get detectedLanguage { - return detections.detections.firstOrNull?.langCode ?? - LanguageKeys.unknownLanguage; - } - // reconstruct fullText based on accepted match //update offsets in existing matches to reflect the change //if existing matches overlap with the accepted one, remove them?? @@ -198,72 +128,8 @@ class IGCTextData { final fullText = newStart + replacement.value.characters + newEnd; originalInput = fullText.toString(); - int startIndex; - int endIndex; - - // replace the tokens that are part of the match - // with the tokens in the replacement - // start is inclusive - try { - startIndex = tokenIndexByOffset(pangeaMatch.match.offset); - // end is exclusive, hence the +1 - // use pangeaMatch.matchContent.trim().length instead of pangeaMatch.match.length since pangeaMatch.match.length may include leading/trailing spaces - endIndex = tokenIndexByOffset( - pangeaMatch.match.offset + pangeaMatch.matchContent.trim().length, - ) + - 1; - } catch (err, s) { - matches.removeAt(matchIndex); - - for (final match in matches) { - match.match.fullText = originalInput; - if (match.match.offset > pangeaMatch.match.offset) { - match.match.offset += - replacement.value.length - pangeaMatch.match.length; - } - } - ErrorHandler.logError( - e: err, - s: s, - data: { - "cursorOffset": pangeaMatch.match.offset, - "match": pangeaMatch.match.toJson(), - "tokens": tokens.map((e) => e.toJson()).toString(), - }, - ); - return; - } - - // for all tokens after the replacement, update their offsets - for (int i = endIndex; i < tokens.length; i++) { - tokens[i].text.offset += - replacement.value.length - pangeaMatch.match.length; - } - - // clone the list for debugging purposes - final List newTokens = List.from(tokens); - - // replace the tokens in the list - newTokens.replaceRange(startIndex, endIndex, replacement.tokens); - - final String newFullText = PangeaToken.reconstructText(newTokens); - if (newFullText.trim() != originalInput.trim() && kDebugMode) { - PangeaToken.reconstructText(newTokens, debugWalkThrough: true); - ErrorHandler.logError( - m: "reconstructed text not working", - s: StackTrace.current, - data: { - "originalInput": originalInput, - "newFullText": newFullText, - "match": pangeaMatch.match.toJson(), - }, - ); - } - - tokens = newTokens; - - //update offsets in existing matches to reflect the change - //Question - remove matches that overlap with the accepted one? + // update offsets in existing matches to reflect the change + // Question - remove matches that overlap with the accepted one? // see case of "quiero ver un fix" matches.removeAt(matchIndex); @@ -276,23 +142,6 @@ class IGCTextData { } } - void removeMatchByOffset(int offset) { - final int index = getTopMatchIndexForOffset(offset); - if (index != -1) { - matches.removeAt(index); - } - } - - int tokenIndexByOffset(int cursorOffset) { - final tokenIndex = tokens.indexWhere( - (token) => token.start <= cursorOffset && cursorOffset <= token.end, - ); - if (tokenIndex < 0) { - throw "No token found for cursor offset"; - } - return tokenIndex; - } - List matchIndicesByOffset(int offset) { final List matchesForOffset = []; for (final (index, match) in matches.indexed) { @@ -314,34 +163,6 @@ class IGCTextData { return matchesForToken[matchIndex]; } - PangeaMatch? getTopMatchForToken(PangeaToken token) { - final int topMatchIndex = getTopMatchIndexForOffset(token.text.offset); - if (topMatchIndex == -1) return null; - return matches[topMatchIndex]; - } - - int getAfterTokenSpacingByIndex(int tokenIndex) { - final int endOfToken = tokens[tokenIndex].end; - - if (tokenIndex + 1 < tokens.length) { - final spaceBetween = tokens[tokenIndex + 1].text.offset - endOfToken; - - if (spaceBetween < 0) { - ErrorHandler.logError( - m: "weird token lengths for ${tokens[tokenIndex].text.content} and ${tokens[tokenIndex + 1].text.content}", - data: { - "fullText": originalInput, - "tokens": tokens.map((e) => e.toJson()).toString(), - }, - ); - return 0; - } - return spaceBetween; - } else { - return originalInput.length - endOfToken; - } - } - static TextStyle underlineStyle(Color color) => TextStyle( decoration: TextDecoration.underline, decorationColor: color, @@ -459,19 +280,4 @@ class IGCTextData { return items; } - - List matchTokens(int matchIndex) { - if (matchIndex >= matches.length) { - return []; - } - - final PangeaMatch match = matches[matchIndex]; - final List tokensForMatch = []; - for (final token in tokens) { - if (match.isOffsetInMatchSpan(token.text.offset)) { - tokensForMatch.add(token); - } - } - return tokensForMatch; - } } diff --git a/lib/pangea/choreographer/models/it_response_model.dart b/lib/pangea/choreographer/models/it_response_model.dart index 08c65a18f..b10be5b24 100644 --- a/lib/pangea/choreographer/models/it_response_model.dart +++ b/lib/pangea/choreographer/models/it_response_model.dart @@ -4,8 +4,6 @@ import 'package:collection/collection.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart'; -import 'package:fluffychat/pangea/common/constants/model_keys.dart'; -import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; class ITResponseModel { String fullTextTranslation; @@ -80,7 +78,7 @@ class Continuance { double probability; int level; String text; - List tokens; + // List tokens; /// saving this in a full json form String description; @@ -100,18 +98,18 @@ class Continuance { required this.inDictionary, required this.hasInfo, required this.gold, - required this.tokens, + // required this.tokens, }); factory Continuance.fromJson(Map json) { - final List tokensInternal = (json[ModelKey.tokens] != null) - ? (json[ModelKey.tokens] as Iterable) - .map( - (e) => PangeaToken.fromJson(e as Map), - ) - .toList() - .cast() - : []; + // final List tokensInternal = (json[ModelKey.tokens] != null) + // ? (json[ModelKey.tokens] as Iterable) + // .map( + // (e) => PangeaToken.fromJson(e as Map), + // ) + // .toList() + // .cast() + // : []; return Continuance( probability: json['probability'].toDouble(), level: json['level'], @@ -122,7 +120,7 @@ class Continuance { wasClicked: json['clkd'] ?? false, hasInfo: json['has_info'] ?? false, gold: json['gold'] ?? false, - tokens: tokensInternal, + // tokens: tokensInternal, ); } @@ -132,7 +130,7 @@ class Continuance { data['level'] = level; data['text'] = text; data['clkd'] = wasClicked; - data[ModelKey.tokens] = tokens.map((e) => e.toJson()).toList(); + // data[ModelKey.tokens] = tokens.map((e) => e.toJson()).toList(); if (!condensed) { data['description'] = description; diff --git a/lib/pangea/choreographer/models/span_data.dart b/lib/pangea/choreographer/models/span_data.dart index 279787fa6..c3bfc3169 100644 --- a/lib/pangea/choreographer/models/span_data.dart +++ b/lib/pangea/choreographer/models/span_data.dart @@ -8,8 +8,6 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; -import 'package:fluffychat/pangea/common/constants/model_keys.dart'; -import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import '../enums/span_choice_type.dart'; import '../enums/span_data_type.dart'; @@ -75,7 +73,7 @@ class SpanChoice { bool selected; String? feedback; DateTime? timestamp; - List tokens; + // List tokens; SpanChoice({ required this.value, @@ -83,18 +81,18 @@ class SpanChoice { this.feedback, this.selected = false, this.timestamp, - this.tokens = const [], + // this.tokens = const [], }); factory SpanChoice.fromJson(Map json) { - final List tokensInternal = (json[ModelKey.tokens] != null) - ? (json[ModelKey.tokens] as Iterable) - .map( - (e) => PangeaToken.fromJson(e as Map), - ) - .toList() - .cast() - : []; + // final List tokensInternal = (json[ModelKey.tokens] != null) + // ? (json[ModelKey.tokens] as Iterable) + // .map( + // (e) => PangeaToken.fromJson(e as Map), + // ) + // .toList() + // .cast() + // : []; return SpanChoice( value: json['value'] as String, type: json['type'] != null @@ -107,7 +105,7 @@ class SpanChoice { selected: json['selected'] ?? false, timestamp: json['timestamp'] != null ? DateTime.parse(json['timestamp']) : null, - tokens: tokensInternal, + // tokens: tokensInternal, ); } @@ -117,7 +115,7 @@ class SpanChoice { 'selected': selected, 'feedback': feedback, 'timestamp': timestamp?.toIso8601String(), - 'tokens': tokens.map((e) => e.toJson()).toList(), + // 'tokens': tokens.map((e) => e.toJson()).toList(), }; String feedbackToDisplay(BuildContext context) { diff --git a/lib/pangea/choreographer/repo/igc_repo.dart b/lib/pangea/choreographer/repo/igc_repo.dart index 5faafc501..72d82e87d 100644 --- a/lib/pangea/choreographer/repo/igc_repo.dart +++ b/lib/pangea/choreographer/repo/igc_repo.dart @@ -2,14 +2,9 @@ import 'dart:convert'; import 'package:http/http.dart'; -import 'package:fluffychat/pangea/choreographer/models/language_detection_model.dart'; import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; -import 'package:fluffychat/pangea/choreographer/repo/language_detection_repo.dart'; import 'package:fluffychat/pangea/choreographer/repo/span_data_repo.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; -import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; -import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; -import 'package:fluffychat/pangea/lemmas/lemma.dart'; import '../../common/constants/model_keys.dart'; import '../../common/network/requests.dart'; import '../../common/network/urls.dart'; @@ -41,42 +36,6 @@ class IgcRepo { await Future.delayed(const Duration(seconds: 2)); final IGCTextData igcTextData = IGCTextData( - detections: LanguageDetectionResponse( - detections: [LanguageDetection(langCode: "en", confidence: 0.99)], - fullText: "This be a sample text", - ), - tokens: [ - PangeaToken( - text: PangeaTokenText(content: "This", offset: 0, length: 4), - lemma: Lemma(form: "This", text: "this", saveVocab: true), - pos: "DET", - morph: {}, - ), - PangeaToken( - text: PangeaTokenText(content: "be", offset: 5, length: 2), - lemma: Lemma(form: "be", text: "be", saveVocab: true), - pos: "VERB", - morph: {}, - ), - PangeaToken( - text: PangeaTokenText(content: "a", offset: 8, length: 1), - lemma: Lemma(form: "a", text: "a", saveVocab: true), - pos: "DET", - morph: {}, - ), - PangeaToken( - text: PangeaTokenText(content: "sample", offset: 10, length: 6), - lemma: Lemma(form: "sample", text: "sample", saveVocab: true), - pos: "NOUN", - morph: {}, - ), - PangeaToken( - text: PangeaTokenText(content: "text", offset: 17, length: 4), - lemma: Lemma(form: "text", text: "text", saveVocab: true), - pos: "NOUN", - morph: {}, - ), - ], matches: [ PangeaMatch( match: spanDataRepomockSpan, diff --git a/lib/pangea/choreographer/repo/interactive_translation_repo.dart b/lib/pangea/choreographer/repo/interactive_translation_repo.dart index 64de559b5..645616085 100644 --- a/lib/pangea/choreographer/repo/interactive_translation_repo.dart +++ b/lib/pangea/choreographer/repo/interactive_translation_repo.dart @@ -8,7 +8,6 @@ import '../../common/network/requests.dart'; import '../../common/network/urls.dart'; import '../models/custom_input_translation_model.dart'; import '../models/it_response_model.dart'; -import 'system_choice_translation_model.dart'; class ITRepo { static Future customInputTranslate( @@ -26,19 +25,19 @@ class ITRepo { return ITResponseModel.fromJson(json); } - static Future systemChoiceTranslate( - SystemChoiceRequestModel subseqText, - ) async { - final Requests req = Requests( - choreoApiKey: Environment.choreoApiKey, - accessToken: MatrixState.pangeaController.userController.accessToken, - ); + // static Future systemChoiceTranslate( + // SystemChoiceRequestModel subseqText, + // ) async { + // final Requests req = Requests( + // choreoApiKey: Environment.choreoApiKey, + // accessToken: MatrixState.pangeaController.userController.accessToken, + // ); - final Response res = - await req.post(url: PApiUrls.subseqStep, body: subseqText.toJson()); + // final Response res = + // await req.post(url: PApiUrls.subseqStep, body: subseqText.toJson()); - final decodedBody = jsonDecode(utf8.decode(res.bodyBytes).toString()); + // final decodedBody = jsonDecode(utf8.decode(res.bodyBytes).toString()); - return ITResponseModel.fromJson(decodedBody); - } + // return ITResponseModel.fromJson(decodedBody); + // } } diff --git a/lib/pangea/choreographer/repo/tokens_repo.dart b/lib/pangea/choreographer/repo/tokens_repo.dart new file mode 100644 index 000000000..9563bc04a --- /dev/null +++ b/lib/pangea/choreographer/repo/tokens_repo.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; + +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/common/network/requests.dart'; +import 'package:fluffychat/pangea/common/network/urls.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/events/repo/token_api_models.dart'; + +class TokensRepo { + static Future get( + String? accessToken, { + required TokensRequestModel request, + }) async { + final Requests req = Requests( + choreoApiKey: Environment.choreoApiKey, + accessToken: accessToken, + ); + + final Response res = await req.post( + url: PApiUrls.tokenize, + body: request.toJson(), + ); + + final TokensResponseModel response = TokensResponseModel.fromJson( + jsonDecode( + utf8.decode(res.bodyBytes).toString(), + ), + ); + + if (response.tokens.isEmpty) { + ErrorHandler.logError( + e: Exception( + "empty tokens in tokenize response return", + ), + data: { + "accessToken": accessToken, + "request": request.toJson(), + }, + ); + } + + return response; + } +} diff --git a/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart b/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart new file mode 100644 index 000000000..dafde919d --- /dev/null +++ b/lib/pangea/choreographer/widgets/igc/message_analytics_feedback.dart @@ -0,0 +1,315 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class MessageAnalyticsFeedback extends StatefulWidget { + final String overlayId; + final int newGrammarConstructs; + final int newVocabConstructs; + + const MessageAnalyticsFeedback({ + required this.overlayId, + required this.newGrammarConstructs, + required this.newVocabConstructs, + super.key, + }); + + @override + State createState() => + MessageAnalyticsFeedbackState(); +} + +class MessageAnalyticsFeedbackState extends State + with TickerProviderStateMixin { + late AnimationController _vocabController; + late AnimationController _grammarController; + late AnimationController _bubbleController; + + late Animation _vocabOpacity; + late Animation _grammarOpacity; + late Animation _scaleAnimation; + late Animation _opacityAnimation; + + static const counterDelay = Duration(milliseconds: 400); + + @override + void initState() { + super.initState(); + _grammarController = AnimationController( + vsync: this, + duration: FluffyThemes.animationDuration, + ); + + _grammarOpacity = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _grammarController, curve: Curves.easeInOut), + ); + + _vocabController = AnimationController( + vsync: this, + duration: FluffyThemes.animationDuration, + ); + + _vocabOpacity = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _vocabController, curve: Curves.easeInOut), + ); + + _bubbleController = AnimationController( + vsync: this, + duration: FluffyThemes.animationDuration, + ); + + _scaleAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _bubbleController, curve: Curves.easeInOut), + ); + + _opacityAnimation = Tween(begin: 0.0, end: 0.9).animate( + CurvedAnimation(parent: _bubbleController, curve: Curves.easeInOut), + ); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _bubbleController.forward(); + + Future.delayed(counterDelay, () { + if (mounted) { + _vocabController.forward(); + _grammarController.forward(); + } + }); + + Future.delayed(const Duration(milliseconds: 4000), () { + if (!mounted) return; + _bubbleController.reverse().then((_) { + MatrixState.pAnyState.closeOverlay(widget.overlayId); + }); + }); + }); + } + + @override + void dispose() { + _vocabController.dispose(); + _grammarController.dispose(); + _bubbleController.dispose(); + super.dispose(); + } + + void _showAnalyticsDialog(ConstructTypeEnum? type) { + showDialog( + context: context, + builder: (context) => AnalyticsPopupWrapper( + view: type ?? ConstructTypeEnum.vocab, + ), + ); + } + + @override + Widget build(BuildContext context) { + if (widget.newVocabConstructs <= 0 && widget.newGrammarConstructs <= 0) { + return const SizedBox.shrink(); + } + + final theme = Theme.of(context); + return Material( + type: MaterialType.transparency, + child: InkWell( + onTap: () => _showAnalyticsDialog(null), + child: ScaleTransition( + scale: _scaleAnimation, + alignment: Alignment.bottomRight, + child: AnimatedBuilder( + animation: _bubbleController, + builder: (context, child) { + return Container( + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest + .withAlpha((_opacityAnimation.value * 255).round()), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + bottomLeft: Radius.circular(16.0), + bottomRight: Radius.circular(4.0), + ), + ), + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.newVocabConstructs > 0) + NewConstructsBadge( + controller: _vocabController, + opacityAnimation: _vocabOpacity, + newConstructs: widget.newVocabConstructs, + type: ConstructTypeEnum.vocab, + tooltip: L10n.of(context).newVocab, + onTap: () => _showAnalyticsDialog( + ConstructTypeEnum.vocab, + ), + ), + if (widget.newGrammarConstructs > 0) + NewConstructsBadge( + controller: _grammarController, + opacityAnimation: _grammarOpacity, + newConstructs: widget.newGrammarConstructs, + type: ConstructTypeEnum.morph, + tooltip: L10n.of(context).newGrammar, + onTap: () => _showAnalyticsDialog( + ConstructTypeEnum.morph, + ), + ), + ], + ), + ); + }, + ), + ), + ), + ); + } +} + +class NewConstructsBadge extends StatelessWidget { + final AnimationController controller; + final Animation opacityAnimation; + + final int newConstructs; + final ConstructTypeEnum type; + final String tooltip; + final VoidCallback onTap; + + const NewConstructsBadge({ + required this.controller, + required this.opacityAnimation, + required this.newConstructs, + required this.type, + required this.tooltip, + required this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Tooltip( + message: tooltip, + child: AnimatedBuilder( + animation: controller, + builder: (context, child) { + return Opacity( + opacity: opacityAnimation.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: ValueKey("$type-counter"), + endValue: newConstructs, + startAnimation: opacityAnimation.value > 0.9, + style: TextStyle( + color: ProgressIndicatorEnum.morphsUsed.color(context), + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ); + }, + ), + ), + ); + } +} + +class AnimatedCounter extends StatefulWidget { + final int endValue; + final TextStyle? style; + final bool startAnimation; + + const AnimatedCounter({ + super.key, + required this.endValue, + this.style, + this.startAnimation = true, + }); + + @override + State createState() => _AnimatedCounterState(); +} + +class _AnimatedCounterState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: FluffyThemes.animationDuration, + ); + + _animation = IntTween( + begin: 0, + end: widget.endValue, + ).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeOutCubic, + ), + ); + + if (widget.startAnimation) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _controller.forward(); + }); + } + } + + @override + void didUpdateWidget(AnimatedCounter oldWidget) { + super.didUpdateWidget(oldWidget); + if (!oldWidget.startAnimation && widget.startAnimation && !_hasAnimated) { + _controller.forward(); + } + } + + bool get _hasAnimated => _controller.isCompleted || _controller.isAnimating; + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Text( + "+ ${_animation.value}", + style: widget.style, + ); + }, + ); + } +} diff --git a/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart b/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart index 054d83913..9da04d969 100644 --- a/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart +++ b/lib/pangea/choreographer/widgets/igc/pangea_text_controller.dart @@ -72,15 +72,6 @@ class PangeaTextController extends TextEditingController { return; } - int tokenIndex; - try { - tokenIndex = choreographer.igc.igcTextData!.tokenIndexByOffset( - selection.baseOffset, - ); - } catch (_) { - return; - } - final int matchIndex = choreographer.igc.igcTextData!.getTopMatchIndexForOffset( selection.baseOffset, @@ -102,9 +93,7 @@ class PangeaTextController extends TextEditingController { matchIndex: matchIndex, onReplacementSelect: choreographer.onReplacementSelect, // may not need this - onSentenceRewrite: ((sentenceRewrite) async { - debugPrint("onSentenceRewrite $tokenIndex $sentenceRewrite"); - }), + onSentenceRewrite: ((sentenceRewrite) async {}), onIgnore: () => choreographer.onIgnoreMatch( cursorOffset: selection.baseOffset, ), diff --git a/lib/pangea/choreographer/widgets/igc/span_card.dart b/lib/pangea/choreographer/widgets/igc/span_card.dart index a25301196..d6c32a98c 100644 --- a/lib/pangea/choreographer/widgets/igc/span_card.dart +++ b/lib/pangea/choreographer/widgets/igc/span_card.dart @@ -6,14 +6,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/bot/utils/bot_style.dart'; import 'package:fluffychat/pangea/choreographer/enums/span_data_type.dart'; import 'package:fluffychat/pangea/choreographer/models/span_data.dart'; import 'package:fluffychat/pangea/choreographer/utils/match_copy.dart'; import 'package:fluffychat/pangea/choreographer/widgets/igc/card_error_widget.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; import '../../../../widgets/matrix.dart'; import '../../../bot/widgets/bot_face_svg.dart'; @@ -153,18 +151,6 @@ class SpanCardState extends State { Future onChoiceSelect(int index) async { selectedChoiceIndex = index; if (selectedChoice != null) { - if (!selectedChoice!.selected) { - MatrixState.pangeaController.putAnalytics.addDraftUses( - selectedChoice!.tokens, - widget.roomId, - selectedChoice!.isBestCorrection - ? ConstructUseTypeEnum.corIGC - : ConstructUseTypeEnum.incIGC, - targetID: - "${selectedChoice!.value}${widget.scm.pangeaMatch?.hashCode.toString()}", - ); - } - selectedChoice!.timestamp = DateTime.now(); selectedChoice!.selected = true; setState( @@ -175,28 +161,7 @@ class SpanCardState extends State { } } - /// Returns the list of distractor choices that are not selected - List? get ignoredMatches => widget.scm.pangeaMatch?.match.choices - ?.where((choice) => choice.isDistractor && !choice.selected) - .toList(); - - /// Returns the list of tokens from choices that are not selected - List? get ignoredTokens => ignoredMatches - ?.expand((choice) => choice.tokens) - .toList() - .cast(); - - /// Adds the ignored tokens to locally cached analytics - void addIgnoredTokenUses() { - MatrixState.pangeaController.putAnalytics.addDraftUses( - ignoredTokens ?? [], - widget.roomId, - ConstructUseTypeEnum.ignIGC, - ); - } - Future onReplaceSelected() async { - addIgnoredTokenUses(); await widget.scm.onReplacementSelect( matchIndex: widget.scm.matchIndex, choiceIndex: selectedChoiceIndex!, @@ -205,8 +170,6 @@ class SpanCardState extends State { } void onIgnoreMatch() { - addIgnoredTokenUses(); - Future.delayed( Duration.zero, () { diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index c7a19f75f..6f02ade8e 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -4,14 +4,11 @@ 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'; import 'package:fluffychat/pangea/choreographer/controllers/it_controller.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_bar_buttons.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart'; -import 'package:fluffychat/pangea/choreographer/widgets/translation_finished_flow.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'; @@ -230,22 +227,7 @@ class ITBarState extends State with SingleTickerProviderStateMixin { controller: itController, ) : itController.isTranslationDone - ? TranslationFeedback( - controller: itController, - vocabCount: itController - .choreographer.altTranslator - .countNewConstructs( - ConstructTypeEnum.vocab, - ), - grammarCount: itController - .choreographer.altTranslator - .countNewConstructs( - ConstructTypeEnum.morph, - ), - feedbackText: itController - .choreographer.altTranslator - .getDefaultFeedback(context), - ) + ? const SizedBox() : ITChoices(controller: itController), ), ), @@ -394,17 +376,6 @@ class ITChoices extends StatelessWidget { continuance.feedbackText(context), ); } - if (!continuance.wasClicked) { - controller.choreographer.pangeaController.putAnalytics.addDraftUses( - continuance.tokens, - controller.choreographer.roomId, - continuance.level > 1 - ? ConstructUseTypeEnum.incIt - : ConstructUseTypeEnum.corIt, - targetID: - "${continuance.text.trim()}${controller.currentITStep.hashCode.toString()}", - ); - } controller.currentITStep!.continuances[index].wasClicked = true; controller.choreographer.setState(); } diff --git a/lib/pangea/choreographer/widgets/start_igc_button.dart b/lib/pangea/choreographer/widgets/start_igc_button.dart index f8a633d8a..a1d2b34c0 100644 --- a/lib/pangea/choreographer/widgets/start_igc_button.dart +++ b/lib/pangea/choreographer/widgets/start_igc_button.dart @@ -94,10 +94,7 @@ class StartIGCButtonState extends State ); return; case AssistanceState.notFetched: - await widget.controller.choreographer.getLanguageHelp( - onlyTokensAndLanguageDetection: false, - manual: true, - ); + await widget.controller.choreographer.getLanguageHelp(manual: true); _showFirstMatch(); return; case AssistanceState.fetched: diff --git a/lib/pangea/choreographer/widgets/translation_finished_flow.dart b/lib/pangea/choreographer/widgets/translation_finished_flow.dart deleted file mode 100644 index bc10f9de8..000000000 --- a/lib/pangea/choreographer/widgets/translation_finished_flow.dart +++ /dev/null @@ -1,379 +0,0 @@ -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'; - -class TranslationFeedback extends StatefulWidget { - final int vocabCount; - final int grammarCount; - final String feedbackText; - final ITController controller; - - const TranslationFeedback({ - super.key, - required this.controller, - required this.vocabCount, - required this.grammarCount, - 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) { - try { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 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) - 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 - 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: 8.0), - ], - ); - } catch (err, stack) { - debugPrint("Error in TranslationFeedback: $err"); - ErrorHandler.logError( - e: err, - 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, - ); - }, - ); - } -} diff --git a/lib/pangea/events/controllers/message_data_controller.dart b/lib/pangea/events/controllers/message_data_controller.dart index ac0c99a48..1421b187d 100644 --- a/lib/pangea/events/controllers/message_data_controller.dart +++ b/lib/pangea/events/controllers/message_data_controller.dart @@ -1,18 +1,14 @@ import 'dart:async'; -import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:http/http.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/choreographer/repo/full_text_translation_repo.dart'; -import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/choreographer/repo/tokens_repo.dart'; import 'package:fluffychat/pangea/common/controllers/base_controller.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/common/network/requests.dart'; -import 'package:fluffychat/pangea/common/network/urls.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; @@ -56,42 +52,6 @@ class MessageDataController extends BaseController { super.dispose(); } - /// get tokens from the server - static Future _fetchTokens( - String accessToken, - TokensRequestModel request, - ) async { - final Requests req = Requests( - choreoApiKey: Environment.choreoApiKey, - accessToken: accessToken, - ); - - final Response res = await req.post( - url: PApiUrls.tokenize, - body: request.toJson(), - ); - - final TokensResponseModel response = TokensResponseModel.fromJson( - jsonDecode( - utf8.decode(res.bodyBytes).toString(), - ), - ); - - if (response.tokens.isEmpty) { - ErrorHandler.logError( - e: Exception( - "empty tokens in tokenize response return", - ), - data: { - "accessToken": accessToken, - "request": request.toJson(), - }, - ); - } - - return response; - } - /// get tokens from the server /// if repEventId is not null, send the tokens to the room Future> _getTokens({ @@ -99,9 +59,9 @@ class MessageDataController extends BaseController { required TokensRequestModel req, required Room? room, }) async { - final TokensResponseModel res = await _fetchTokens( + final TokensResponseModel res = await TokensRepo.get( _pangeaController.userController.accessToken, - req, + request: req, ); if (repEventId != null && room != null) { room @@ -234,9 +194,9 @@ class MessageDataController extends BaseController { required TokensRequestModel req, required Room room, }) async { - final TokensResponseModel res = await _fetchTokens( + final TokensResponseModel res = await TokensRepo.get( _pangeaController.userController.accessToken, - req, + request: req, ); try { diff --git a/lib/pangea/events/models/pangea_token_model.dart b/lib/pangea/events/models/pangea_token_model.dart index c51e00814..05799feb8 100644 --- a/lib/pangea/events/models/pangea_token_model.dart +++ b/lib/pangea/events/models/pangea_token_model.dart @@ -189,6 +189,7 @@ class PangeaToken { OneConstructUse toVocabUse( ConstructUseTypeEnum type, ConstructUseMetaData metadata, + int xp, ) { return OneConstructUse( useType: type, @@ -197,17 +198,19 @@ class PangeaToken { constructType: ConstructTypeEnum.vocab, metadata: metadata, category: pos, + xp: xp, ); } List allUses( ConstructUseTypeEnum type, ConstructUseMetaData metadata, + int xp, ) { final List uses = []; if (!lemma.saveVocab) return uses; - uses.add(toVocabUse(type, metadata)); + uses.add(toVocabUse(type, metadata, xp)); for (final morphFeature in morph.keys) { uses.add( OneConstructUse( @@ -217,6 +220,7 @@ class PangeaToken { constructType: ConstructTypeEnum.morph, metadata: metadata, category: morphFeature, + xp: xp, ), ); } diff --git a/lib/pangea/events/models/representation_content_model.dart b/lib/pangea/events/models/representation_content_model.dart index 893dec42d..cd859c05b 100644 --- a/lib/pangea/events/models/representation_content_model.dart +++ b/lib/pangea/events/models/representation_content_model.dart @@ -1,10 +1,12 @@ +import 'dart:math'; + import 'package:matrix/matrix.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/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart'; import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/toolbar/models/speech_to_text_models.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -132,162 +134,138 @@ class PangeaRepresentation { ) .toList(); } - for (final token in tokensToSave) { - uses.addAll( - _getUsesForToken( - token, - metadata, - choreo: choreo, - ), - ); + + if (choreo == null || + (choreo.choreoSteps.isEmpty && choreo.itSteps.isEmpty)) { + for (final token in tokensToSave) { + uses.addAll( + token.allUses( + ConstructUseTypeEnum.wa, + metadata, + ConstructUseTypeEnum.wa.pointValue, + ), + ); + } + + return uses; } - return uses; - } + for (final token in tokensToSave) { + ChoreoRecordStep? tokenStep; + for (final step in choreo.choreoSteps) { + final igcMatch = step.acceptedOrIgnoredMatch; + final itStep = step.itStep; + if (itStep == null && igcMatch == null) { + continue; + } - /// Returns a [OneConstructUse] for the given [token] - /// If there is no [choreo], the [token] is - /// considered to be a [ConstructUseTypeEnum.wa] as long as it matches the target language. - /// Later on, we may want to consider putting it in some category of like 'pending' - /// - /// For both vocab and morph constructs, we should - /// 1) give wa if no assistance was used - /// 2) give ga if IGC was used and - /// 3) make no use if IT was used - List _getUsesForToken( - PangeaToken token, - ConstructUseMetaData metadata, { - ChoreoRecord? choreo, - }) { - final List uses = []; - final lemma = token.lemma; - final content = token.text.content; - - if (choreo == null) { - uses.add( - OneConstructUse( - useType: ConstructUseTypeEnum.wa, - lemma: token.pos, - form: token.text.content, - category: "POS", - constructType: ConstructTypeEnum.morph, - metadata: metadata, - ), - ); - - for (final entry in token.morph.entries) { - uses.add( - OneConstructUse( - useType: ConstructUseTypeEnum.wa, - lemma: entry.value, - form: token.text.content, - category: entry.key, - constructType: ConstructTypeEnum.morph, - metadata: metadata, - ), + final choices = step.choices; + if (choices == null || choices.isEmpty) { + continue; + } + + final stepContainsToken = choices.any( + (choice) => choice.contains(token.text.content), ); + + // if the step contains the token, and the token hasn't been assigned a step + // (or the assigned step is an IGC step, but an IT step contains the token) + // then assign the token to the step + if (stepContainsToken && + (tokenStep == null || + (tokenStep.itStep == null && step.itStep != null))) { + tokenStep = step; + } } - if (lemma.saveVocab) { - uses.add( - token.toVocabUse( + if (tokenStep == null || + tokenStep.acceptedOrIgnoredMatch?.status == + PangeaMatchStatus.automatic) { + // if the token wasn't found in any IT or IGC step, so it was wa + uses.addAll( + token.allUses( ConstructUseTypeEnum.wa, metadata, + ConstructUseTypeEnum.wa.pointValue, ), ); + continue; } - return uses; - } - for (final step in choreo.choreoSteps) { - /// if 1) accepted match 2) token is in the replacement and 3) replacement - /// is in the overall step text, then token was a ga - final bool isAcceptedMatch = - step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted; - - // if the token was in an IT match, return no uses - if (step.itStep != null) return []; - - // if this step was not accepted, continue - if (!isAcceptedMatch) continue; - - if (isAcceptedMatch && - step.acceptedOrIgnoredMatch?.match.choices != null) { - final choices = step.acceptedOrIgnoredMatch!.match.choices!; - final bool stepContainedToken = choices.any( - (choice) => - // if this choice contains the token's content - choice.value.contains(content), + if (tokenStep.acceptedOrIgnoredMatch != null && + tokenStep.acceptedOrIgnoredMatch?.status != + PangeaMatchStatus.accepted) { + uses.addAll( + token.allUses( + ConstructUseTypeEnum.ga, + metadata, + 0, + ), ); - if (stepContainedToken) { - // give ga if IGC was used - uses.add( - token.toVocabUse( - ConstructUseTypeEnum.ga, - metadata, - ), - ); + continue; + } - uses.add( - OneConstructUse( - useType: ConstructUseTypeEnum.ga, - lemma: token.pos, - form: token.text.content, - category: "POS", - constructType: ConstructTypeEnum.morph, - metadata: metadata, - ), + if (tokenStep.itStep != null) { + final selectedChoices = tokenStep.itStep!.continuances + .where((choice) => choice.wasClicked) + .length; + if (selectedChoices == 0) { + ErrorHandler.logError( + e: "No selected choices for IT step", + data: { + "token": token.text.content, + "step": tokenStep.toJson(), + }, ); + continue; + } + + final corITPoints = ConstructUseTypeEnum.corIt.pointValue; + final incITPoints = ConstructUseTypeEnum.incIt.pointValue; + final xp = max( + 0, + corITPoints + (incITPoints * (selectedChoices - 1)), + ); - for (final entry in token.morph.entries) { - uses.add( - OneConstructUse( - useType: ConstructUseTypeEnum.ga, - lemma: entry.value, - form: token.text.content, - category: entry.key, - constructType: ConstructTypeEnum.morph, - metadata: metadata, - ), - ); - } - return uses; + uses.addAll( + token.allUses( + ConstructUseTypeEnum.ta, + metadata, + xp, + ), + ); + } else if (tokenStep.acceptedOrIgnoredMatch!.match.choices != null) { + final selectedChoices = tokenStep.acceptedOrIgnoredMatch!.match.choices! + .where((choice) => choice.selected) + .length; + if (selectedChoices == 0) { + ErrorHandler.logError( + e: "No selected choices for IGC step", + data: { + "token": token.text.content, + "step": tokenStep.toJson(), + }, + ); + continue; } - } - } - uses.add( - OneConstructUse( - useType: ConstructUseTypeEnum.wa, - lemma: token.pos, - form: token.text.content, - category: "POS", - constructType: ConstructTypeEnum.morph, - metadata: metadata, - ), - ); + final corIGCPoints = ConstructUseTypeEnum.corIGC.pointValue; + final incIGCPoints = ConstructUseTypeEnum.incIGC.pointValue; + final xp = max( + 0, + corIGCPoints + (incIGCPoints * (selectedChoices - 1)), + ); - // the token wasn't found in any IT or IGC step, so it was wa - for (final entry in token.morph.entries) { - uses.add( - OneConstructUse( - useType: ConstructUseTypeEnum.wa, - lemma: entry.value, - form: token.text.content, - category: entry.key, - constructType: ConstructTypeEnum.morph, - metadata: metadata, - ), - ); - } - if (lemma.saveVocab) { - uses.add( - token.toVocabUse( - ConstructUseTypeEnum.wa, - metadata, - ), - ); + uses.addAll( + token.allUses( + ConstructUseTypeEnum.ga, + metadata, + xp, + ), + ); + } } + return uses; } } diff --git a/lib/pangea/practice_activities/practice_activity_model.dart b/lib/pangea/practice_activities/practice_activity_model.dart index 4e314ab2a..5dee155aa 100644 --- a/lib/pangea/practice_activities/practice_activity_model.dart +++ b/lib/pangea/practice_activities/practice_activity_model.dart @@ -8,6 +8,7 @@ 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/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; @@ -103,13 +104,15 @@ class PracticeActivityModel { // "onMultipleChoiceSelect: ${choice.form} ${responseUseType(choice)}", // ); + final constructUseType = + practiceTarget.record.responses.last.useType(activityType); MatrixState.pangeaController.putAnalytics.setState( AnalyticsStream( eventId: event?.eventId, roomId: event?.room.id, constructs: [ OneConstructUse( - useType: practiceTarget.record.responses.last.useType(activityType), + useType: constructUseType, lemma: choice.form.cId.lemma, constructType: choice.form.cId.type, metadata: ConstructUseMetaData( @@ -119,6 +122,7 @@ class PracticeActivityModel { ), category: choice.form.cId.category, form: choice.form.form, + xp: constructUseType.pointValue, ), ], targetID: targetTokens.first.text.uniqueKey, @@ -179,14 +183,15 @@ class PracticeActivityModel { // we don't take off points for incorrect emoji matches if (ActivityTypeEnum.emoji != activityType || isCorrect) { + final constructUseType = + practiceTarget.record.responses.last.useType(activityType); MatrixState.pangeaController.putAnalytics.setState( AnalyticsStream( eventId: event?.eventId, roomId: event?.room.id, constructs: [ OneConstructUse( - useType: - practiceTarget.record.responses.last.useType(activityType), + useType: constructUseType, lemma: token.lemma.text, constructType: ConstructTypeEnum.vocab, metadata: ConstructUseMetaData( @@ -197,6 +202,7 @@ class PracticeActivityModel { category: token.pos, // in the case of a wrong answer, the cId doesn't match the token form: token.text.content, + xp: constructUseType.pointValue, ), ], targetID: token.text.uniqueKey, diff --git a/lib/pangea/practice_activities/practice_record.dart b/lib/pangea/practice_activities/practice_record.dart index 2725c156c..8d7c5dda4 100644 --- a/lib/pangea/practice_activities/practice_record.dart +++ b/lib/pangea/practice_activities/practice_record.dart @@ -220,35 +220,41 @@ class ActivityRecordResponse { case ActivityTypeEnum.wordFocusListening: case ActivityTypeEnum.lemmaId: final token = practiceActivity.targetTokens.first; + final constructUseType = useType(practiceActivity.activityType); return [ OneConstructUse( lemma: token.lemma.text, form: token.text.content, constructType: ConstructTypeEnum.vocab, - useType: useType(practiceActivity.activityType), + useType: constructUseType, metadata: metadata, category: token.pos, + xp: constructUseType.pointValue, ), ]; case ActivityTypeEnum.messageMeaning: + final constructUseType = useType(practiceActivity.activityType); return practiceActivity.targetTokens .expand( (t) => t.allUses( - useType(practiceActivity.activityType), + constructUseType, metadata, + constructUseType.pointValue, ), ) .toList(); case ActivityTypeEnum.hiddenWordListening: + final constructUseType = useType(practiceActivity.activityType); return practiceActivity.targetTokens .map( (token) => OneConstructUse( lemma: token.lemma.text, form: token.text.content, constructType: ConstructTypeEnum.vocab, - useType: useType(practiceActivity.activityType), + useType: constructUseType, metadata: metadata, category: token.pos, + xp: constructUseType.pointValue, ), ) .toList(); @@ -273,13 +279,15 @@ class ActivityRecordResponse { ); return null; } + final constructUseType = useType(practiceActivity.activityType); return OneConstructUse( lemma: tag, form: practiceActivity.targetTokens.first.text.content, constructType: ConstructTypeEnum.morph, - useType: useType(practiceActivity.activityType), + useType: constructUseType, metadata: metadata, category: practiceActivity.morphFeature!, + xp: constructUseType.pointValue, ); }, ) diff --git a/lib/pangea/practice_activities/practice_selection.dart b/lib/pangea/practice_activities/practice_selection.dart index ebe45fec7..de2aa3709 100644 --- a/lib/pangea/practice_activities/practice_selection.dart +++ b/lib/pangea/practice_activities/practice_selection.dart @@ -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; @@ -42,8 +44,6 @@ class PracticeSelection { _tokens.any((t) => t.lemma.saveVocab) && langCode.split("-")[0] == _userL2.split("-")[0]; - String get messageText => PangeaToken.reconstructText(tokens); - Map toJson() => { 'createdAt': createdAt.toIso8601String(), 'lang_code': langCode, diff --git a/lib/pangea/toolbar/reading_assistance_input_row/message_morph_choice.dart b/lib/pangea/toolbar/reading_assistance_input_row/message_morph_choice.dart index 38b6c72ee..ac02caa0e 100644 --- a/lib/pangea/toolbar/reading_assistance_input_row/message_morph_choice.dart +++ b/lib/pangea/toolbar/reading_assistance_input_row/message_morph_choice.dart @@ -68,13 +68,6 @@ class MessageMorphInputBarContentState super.didUpdateWidget(oldWidget); } - String? get _correctChoice { - return widget.activity.multipleChoiceContent?.choices - .firstWhereOrNull((choice) { - return widget.activity.practiceTarget.wasCorrectChoice(choice) == true; - }); - } - TextStyle? textStyle(BuildContext context) => overlay.maxWidth > 600 ? Theme.of(context).textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, diff --git a/pubspec.lock b/pubspec.lock index 5f3b85277..6c1b0bfe3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -114,10 +114,10 @@ packages: dependency: "direct main" description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.11.0" audio_session: dependency: transitive description: @@ -218,10 +218,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" cached_network_image: dependency: "direct main" description: @@ -266,10 +266,10 @@ packages: dependency: "direct main" description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.3.0" charcode: dependency: transitive description: @@ -306,18 +306,18 @@ packages: dependency: transitive description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" collection: dependency: "direct main" description: name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.19.1" + version: "1.19.0" colorize: dependency: transitive description: @@ -506,10 +506,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.1" fcm_shared_isolate: dependency: "direct main" description: @@ -529,10 +529,10 @@ packages: dependency: transitive description: name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.0" file_picker: dependency: "direct main" description: @@ -1390,18 +1390,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -1478,10 +1478,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: @@ -1511,10 +1511,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.15.0" mgrs_dart: dependency: transitive description: @@ -1695,10 +1695,10 @@ packages: dependency: "direct main" description: name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -1823,10 +1823,10 @@ packages: dependency: transitive description: name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.6" + version: "3.1.5" platform_detect: dependency: transitive description: @@ -1903,10 +1903,10 @@ packages: dependency: transitive description: name: process - sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" url: "https://pub.dev" source: hosted - version: "5.0.3" + version: "5.0.2" proj4dart: dependency: transitive description: @@ -2284,10 +2284,10 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.10.0" sprintf: dependency: transitive description: @@ -2364,26 +2364,26 @@ packages: dependency: transitive description: name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.12.1" + version: "1.12.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.3.0" string_validator: dependency: transitive description: @@ -2436,34 +2436,34 @@ packages: dependency: transitive description: name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.1" test: dependency: transitive description: name: test - sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" url: "https://pub.dev" source: hosted - version: "1.25.15" + version: "1.25.8" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.3" test_core: dependency: transitive description: name: test_core - sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" url: "https://pub.dev" source: hosted - version: "0.6.8" + version: "0.6.5" text_to_speech: dependency: "direct main" description: @@ -2757,10 +2757,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "14.3.0" wakelock_plus: dependency: "direct main" description: @@ -2890,5 +2890,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.27.0"