From f6d7bfa9813046069492656108cce9153f1563af Mon Sep 17 00:00:00 2001 From: wcjord <32568597+wcjord@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:07:49 -0500 Subject: [PATCH] fix(analytics): emit granular IGC/IT use types instead of collapsed ga/ta (#5858) * fix: emit granular IGC/IT use types instead of collapsed ga/ta * formatting * fix linter issues with deprecated use types * fix: don't add match viewing update to choreo record, don't flatten token IGC uses into a single type * break vocabAndMorphUses down into smaller functions * filter viewed choreo steps when getting uses from choreo --------- Co-authored-by: ggurdin --- .../space_analytics_summary_model.dart | 8 + .../construct_use_type_enum.dart | 24 +- .../grammar_error_target_generator.dart | 7 +- .../choreographer/choreo_record_model.dart | 8 + .../models/representation_content_model.dart | 274 +++++++++++------- test/pangea/practice_target_scorer_test.dart | 32 +- 6 files changed, 238 insertions(+), 115 deletions(-) diff --git a/lib/pangea/analytics_downloads/space_analytics_summary_model.dart b/lib/pangea/analytics_downloads/space_analytics_summary_model.dart index bc3a05cd9..f7bc5613f 100644 --- a/lib/pangea/analytics_downloads/space_analytics_summary_model.dart +++ b/lib/pangea/analytics_downloads/space_analytics_summary_model.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/material.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -222,6 +224,12 @@ class SpaceAnalyticsSummaryModel { ConstructUseTypeEnum.wa, ConstructUseTypeEnum.ga, ConstructUseTypeEnum.ta, + ConstructUseTypeEnum.corIt, + ConstructUseTypeEnum.incIt, + ConstructUseTypeEnum.ignIt, + ConstructUseTypeEnum.corIGC, + ConstructUseTypeEnum.incIGC, + ConstructUseTypeEnum.ignIGC, }; final List morphConstructs = []; diff --git a/lib/pangea/analytics_misc/construct_use_type_enum.dart b/lib/pangea/analytics_misc/construct_use_type_enum.dart index a645d7721..494079871 100644 --- a/lib/pangea/analytics_misc/construct_use_type_enum.dart +++ b/lib/pangea/analytics_misc/construct_use_type_enum.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/material.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -10,9 +12,15 @@ enum ConstructUseTypeEnum { wa, /// produced during IGC + @Deprecated( + 'Use corIGC/incIGC/ignIGC instead. Kept for backward compat with stored events.', + ) ga, /// produced during IT + @Deprecated( + 'Use corIt/incIt/ignIt instead. Kept for backward compat with stored events.', + ) ta, /// produced in chat by user and igc was not run @@ -463,23 +471,35 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { } } - /// Whether this use type represents direct chat production (wa, ga, ta). + /// Whether this use type represents direct chat production. bool get isChatUse { switch (this) { case ConstructUseTypeEnum.wa: case ConstructUseTypeEnum.ga: case ConstructUseTypeEnum.ta: + case ConstructUseTypeEnum.corIt: + case ConstructUseTypeEnum.incIt: + case ConstructUseTypeEnum.ignIt: + case ConstructUseTypeEnum.corIGC: + case ConstructUseTypeEnum.incIGC: + case ConstructUseTypeEnum.ignIGC: return true; default: return false; } } - /// Whether this chat use involved assistance (ga = IGC, ta = IT). + /// Whether this chat use involved assistance (IGC or IT). bool get isAssistedChatUse { switch (this) { case ConstructUseTypeEnum.ga: case ConstructUseTypeEnum.ta: + case ConstructUseTypeEnum.corIt: + case ConstructUseTypeEnum.incIt: + case ConstructUseTypeEnum.ignIt: + case ConstructUseTypeEnum.corIGC: + case ConstructUseTypeEnum.incIGC: + case ConstructUseTypeEnum.ignIGC: return true; default: return false; diff --git a/lib/pangea/analytics_practice/grammar_error_target_generator.dart b/lib/pangea/analytics_practice/grammar_error_target_generator.dart index 2d6e01dfa..83ae65418 100644 --- a/lib/pangea/analytics_practice/grammar_error_target_generator.dart +++ b/lib/pangea/analytics_practice/grammar_error_target_generator.dart @@ -33,7 +33,12 @@ class GrammarErrorTargetGenerator { } final errorUses = construct.cappedUses.where( - (u) => u.useType == ConstructUseTypeEnum.ga, + (u) => + // ignore: deprecated_member_use_from_same_package + u.useType == ConstructUseTypeEnum.ga || + u.useType == ConstructUseTypeEnum.corIGC || + u.useType == ConstructUseTypeEnum.ignIGC || + u.useType == ConstructUseTypeEnum.incIGC, ); if (errorUses.isEmpty) continue; diff --git a/lib/pangea/choreographer/choreo_record_model.dart b/lib/pangea/choreographer/choreo_record_model.dart index 47241c905..84d7ccdd4 100644 --- a/lib/pangea/choreographer/choreo_record_model.dart +++ b/lib/pangea/choreographer/choreo_record_model.dart @@ -301,6 +301,14 @@ class ChoreoRecordStepModel { .toList() .cast(); } + + String? get selectedChoice { + if (itStep != null) { + return itStep!.chosenContinuance?.text; + } + + return acceptedOrIgnoredMatch?.match.selectedChoice?.value; + } } // Example flow diff --git a/lib/pangea/events/models/representation_content_model.dart b/lib/pangea/events/models/representation_content_model.dart index e758635d4..bfffcd231 100644 --- a/lib/pangea/events/models/representation_content_model.dart +++ b/lib/pangea/events/models/representation_content_model.dart @@ -1,12 +1,12 @@ -import 'dart:math'; - import 'package:matrix/matrix.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/choreo_record_model.dart'; +import 'package:fluffychat/pangea/choreographer/completed_it_step_model.dart'; +import 'package:fluffychat/pangea/choreographer/igc/pangea_match_model.dart'; import 'package:fluffychat/pangea/choreographer/igc/pangea_match_status_enum.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/choreographer/igc/span_choice_type_enum.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/speech_to_text/speech_to_text_response_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -108,12 +108,10 @@ class PangeaRepresentation { ConstructUseMetaData? metadata, ChoreoRecordModel? choreo, }) { - final List uses = []; - // missing vital info so return if (event?.roomId == null && metadata?.roomId == null) { // debugger(when: kDebugMode); - return uses; + return []; } metadata ??= ConstructUseMetaData( @@ -122,22 +120,9 @@ class PangeaRepresentation { timeStamp: event.originServerTs, ); - // for each token, record whether selected in ga, ta, or wa - List tokensToSave = tokens - .where((token) => token.lemma.saveVocab) - .toList(); - if (choreo != null && choreo.pastedStrings.isNotEmpty) { - tokensToSave = tokensToSave - .where( - (token) => !choreo.pastedStrings.any( - (pasted) => pasted.toLowerCase().contains( - token.text.content.toLowerCase(), - ), - ), - ) - .toList(); - } + final tokensToSave = _filterTokensToSave(tokens, choreo); + final List uses = []; if (choreo == null || choreo.choreoSteps.isEmpty) { for (final token in tokensToSave) { uses.addAll( @@ -148,99 +133,172 @@ class PangeaRepresentation { ), ); } - return uses; } for (final token in tokensToSave) { - ChoreoRecordStepModel? tokenStep; - for (final step in choreo.choreoSteps) { - final igcMatch = step.acceptedOrIgnoredMatch; - final itStep = step.itStep; - if (itStep == null && igcMatch == null) { - continue; - } - - 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 (tokenStep == null || - tokenStep.acceptedOrIgnoredMatch?.status == - PangeaMatchStatusEnum.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; - } - - if (tokenStep.acceptedOrIgnoredMatch != null && - tokenStep.acceptedOrIgnoredMatch?.status != - PangeaMatchStatusEnum.accepted) { - uses.addAll(token.allUses(ConstructUseTypeEnum.ga, metadata, 0)); - continue; - } - - 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))); - - 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; - } - - final corIGCPoints = ConstructUseTypeEnum.corIGC.pointValue; - final incIGCPoints = ConstructUseTypeEnum.incIGC.pointValue; - final xp = max( - 0, - corIGCPoints + (incIGCPoints * (selectedChoices - 1)), - ); - - uses.addAll(token.allUses(ConstructUseTypeEnum.ga, metadata, xp)); - } + final step = _getStepForToken(token, choreo); + uses.addAll(_getUsesForToken(token, metadata, step)); } return uses; } + + List _filterTokensToSave( + List tokens, + ChoreoRecordModel? choreo, + ) { + final List tokensToSave = tokens + .where((token) => token.lemma.saveVocab) + .toList(); + + final pastedStrings = choreo?.pastedStrings ?? {}; + + return tokensToSave + .where( + (token) => !pastedStrings.any( + (pasted) => + pasted.toLowerCase().contains(token.text.content.toLowerCase()), + ), + ) + .toList(); + } + + ChoreoRecordStepModel? _getStepForToken( + PangeaToken token, + ChoreoRecordModel choreo, + ) { + ChoreoRecordStepModel? tokenStep; + for (final step in choreo.choreoSteps) { + final igcMatch = step.acceptedOrIgnoredMatch; + final itStep = step.itStep; + if (itStep == null && + (igcMatch == null || + igcMatch.status == PangeaMatchStatusEnum.viewed)) { + continue; + } + + final choices = step.choices; + if (choices == null || choices.isEmpty) { + continue; + } + + final stepContainsToken = + step.selectedChoice?.contains(token.text.content) == true; + + // 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; + } + } + return tokenStep; + } + + List _getUsesForToken( + PangeaToken token, + ConstructUseMetaData metadata, + ChoreoRecordStepModel? tokenStep, + ) { + if (tokenStep == null || + tokenStep.acceptedOrIgnoredMatch?.status == + PangeaMatchStatusEnum.automatic) { + // if the token wasn't found in any IT or IGC step, so it was wa + return token.allUses( + ConstructUseTypeEnum.wa, + metadata, + ConstructUseTypeEnum.wa.pointValue, + ); + } + + if (tokenStep.acceptedOrIgnoredMatch != null && + tokenStep.acceptedOrIgnoredMatch?.status != + PangeaMatchStatusEnum.accepted) { + return token.allUses(ConstructUseTypeEnum.ignIGC, metadata, 0); + } + + if (tokenStep.itStep != null) { + return _getUsesForITToken(token, tokenStep.itStep!, metadata); + } else if (tokenStep.acceptedOrIgnoredMatch!.match.choices != null) { + return _getUsesForIGCToken( + token, + tokenStep.acceptedOrIgnoredMatch!, + metadata, + ); + } + + return []; + } + + List _getUsesForITToken( + PangeaToken token, + CompletedITStepModel itStep, + ConstructUseMetaData metadata, + ) { + final selectedChoices = itStep.continuances.where( + (choice) => choice.wasClicked, + ); + + if (selectedChoices.isEmpty) { + return token.allUses(ConstructUseTypeEnum.ignIt, metadata, 0); + } + + final numCorrectChoices = selectedChoices + .where((choice) => choice.gold) + .length; + + final numIncorrectChoices = selectedChoices.length - numCorrectChoices; + return [ + if (numCorrectChoices > 0) + ...token.allUses( + ConstructUseTypeEnum.corIt, + metadata, + ConstructUseTypeEnum.corIt.pointValue * numCorrectChoices, + ), + if (numIncorrectChoices > 0) + ...token.allUses( + ConstructUseTypeEnum.incIt, + metadata, + ConstructUseTypeEnum.incIt.pointValue * numIncorrectChoices, + ), + ]; + } + + List _getUsesForIGCToken( + PangeaToken token, + PangeaMatch match, + ConstructUseMetaData metadata, + ) { + final selectedChoices = match.match.choices!.where( + (choice) => choice.selected, + ); + + if (selectedChoices.isEmpty) { + return token.allUses(ConstructUseTypeEnum.ignIGC, metadata, 0); + } + + final numCorrectChoices = selectedChoices + .where((choice) => choice.type.isSuggestion) + .length; + + final numIncorrectChoices = selectedChoices.length - numCorrectChoices; + + return [ + if (numCorrectChoices > 0) + ...token.allUses( + ConstructUseTypeEnum.corIGC, + metadata, + ConstructUseTypeEnum.corIGC.pointValue * numCorrectChoices, + ), + if (numIncorrectChoices > 0) + ...token.allUses( + ConstructUseTypeEnum.incIGC, + metadata, + ConstructUseTypeEnum.incIGC.pointValue * numIncorrectChoices, + ), + ]; + } } diff --git a/test/pangea/practice_target_scorer_test.dart b/test/pangea/practice_target_scorer_test.dart index eaa9e4d0b..2184b966c 100644 --- a/test/pangea/practice_target_scorer_test.dart +++ b/test/pangea/practice_target_scorer_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter_test/flutter_test.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; @@ -54,18 +56,30 @@ ConstructIdentifier _makeId({String lemma = 'test', String category = 'verb'}) { void main() { group('ConstructUseTypeEnum boolean getters', () { - test('isChatUse is true for wa, ga, ta', () { + test('isChatUse is true for wa, ga, ta and granular IGC/IT types', () { expect(ConstructUseTypeEnum.wa.isChatUse, true); expect(ConstructUseTypeEnum.ga.isChatUse, true); expect(ConstructUseTypeEnum.ta.isChatUse, true); + expect(ConstructUseTypeEnum.corIGC.isChatUse, true); + expect(ConstructUseTypeEnum.incIGC.isChatUse, true); + expect(ConstructUseTypeEnum.ignIGC.isChatUse, true); + expect(ConstructUseTypeEnum.corIt.isChatUse, true); + expect(ConstructUseTypeEnum.incIt.isChatUse, true); + expect(ConstructUseTypeEnum.ignIt.isChatUse, true); expect(ConstructUseTypeEnum.corPA.isChatUse, false); expect(ConstructUseTypeEnum.incLM.isChatUse, false); expect(ConstructUseTypeEnum.click.isChatUse, false); }); - test('isAssistedChatUse is true for ga, ta only', () { + test('isAssistedChatUse is true for ga, ta and granular IGC/IT types', () { expect(ConstructUseTypeEnum.ga.isAssistedChatUse, true); expect(ConstructUseTypeEnum.ta.isAssistedChatUse, true); + expect(ConstructUseTypeEnum.corIGC.isAssistedChatUse, true); + expect(ConstructUseTypeEnum.incIGC.isAssistedChatUse, true); + expect(ConstructUseTypeEnum.ignIGC.isAssistedChatUse, true); + expect(ConstructUseTypeEnum.corIt.isAssistedChatUse, true); + expect(ConstructUseTypeEnum.incIt.isAssistedChatUse, true); + expect(ConstructUseTypeEnum.ignIt.isAssistedChatUse, true); expect(ConstructUseTypeEnum.wa.isAssistedChatUse, false); }); @@ -132,12 +146,22 @@ void main() { expect(uses.practiceTier, PracticeTier.active); }); - test('ga use (IGC correction) → active', () { + test('ignIGC use → active', () { + final uses = _makeConstructUses([_makeUse(ConstructUseTypeEnum.ignIGC)]); + expect(uses.practiceTier, PracticeTier.active); + }); + + test('corIt use (IT translation) → active', () { + final uses = _makeConstructUses([_makeUse(ConstructUseTypeEnum.corIt)]); + expect(uses.practiceTier, PracticeTier.active); + }); + + test('ga use (legacy IGC correction) → active', () { final uses = _makeConstructUses([_makeUse(ConstructUseTypeEnum.ga)]); expect(uses.practiceTier, PracticeTier.active); }); - test('ta use (IT translation) → active', () { + test('ta use (legacy IT translation) → active', () { final uses = _makeConstructUses([_makeUse(ConstructUseTypeEnum.ta)]); expect(uses.practiceTier, PracticeTier.active); });