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 <ggurdin@gmail.com>
This commit is contained in:
parent
a370386016
commit
f6d7bfa981
6 changed files with 238 additions and 115 deletions
|
|
@ -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<String> morphConstructs = [];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -301,6 +301,14 @@ class ChoreoRecordStepModel {
|
|||
.toList()
|
||||
.cast<String>();
|
||||
}
|
||||
|
||||
String? get selectedChoice {
|
||||
if (itStep != null) {
|
||||
return itStep!.chosenContinuance?.text;
|
||||
}
|
||||
|
||||
return acceptedOrIgnoredMatch?.match.selectedChoice?.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Example flow
|
||||
|
|
|
|||
|
|
@ -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<OneConstructUse> 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<PangeaToken> 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<OneConstructUse> 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<PangeaToken> _filterTokensToSave(
|
||||
List<PangeaToken> tokens,
|
||||
ChoreoRecordModel? choreo,
|
||||
) {
|
||||
final List<PangeaToken> tokensToSave = tokens
|
||||
.where((token) => token.lemma.saveVocab)
|
||||
.toList();
|
||||
|
||||
final pastedStrings = choreo?.pastedStrings ?? <String>{};
|
||||
|
||||
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<OneConstructUse> _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<OneConstructUse> _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<OneConstructUse> _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,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue