diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index f1c9e5082..06359a24b 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -2,20 +2,15 @@ import 'dart:convert'; import 'dart:developer'; import 'package:collection/collection.dart'; -import 'package:fluffychat/pangea/constants/choreo_constants.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart'; -import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; -import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; -import 'package:fluffychat/pangea/models/lemma.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; -import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/space_model.dart'; import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; @@ -673,158 +668,25 @@ class PangeaMessageEvent { List get allConstructUses => [..._grammarConstructUses, ..._vocabUses, ..._itStepsToConstructUses]; - /// Returns a list of [OneConstructUse] from itSteps for which the continuance - /// was selected or ignored. Correct selections are considered in the tokens - /// flow. Once all continuances have lemmas, we can do both correct and incorrect - /// in this flow. It actually doesn't do anything at all right now, because the - /// choregrapher is not returning lemmas for continuances. This is a TODO. - /// So currently only the lemmas can be gotten from the tokens for choices that - /// are actually in the final message. - List get _itStepsToConstructUses { - final List uses = []; - if (originalSent?.choreo == null) return uses; - - for (final itStep in originalSent!.choreo!.itSteps) { - for (final continuance in itStep.continuances) { - final List tokensToSave = - continuance.tokens.where((t) => t.lemma.saveVocab).toList(); - - if (originalSent!.choreo!.finalMessage.contains(continuance.text)) { - continue; - } - if (continuance.wasClicked) { - //PTODO - account for end of flow score - if (continuance.level != ChoreoConstants.levelThresholdForGreen) { - for (final token in tokensToSave) { - uses.add( - _lemmaToVocabUse( - token.lemma, - ConstructUseTypeEnum.incIt, - ), - ); - } - } - } else { - if (continuance.level != ChoreoConstants.levelThresholdForGreen) { - for (final token in tokensToSave) { - uses.add( - _lemmaToVocabUse( - token.lemma, - ConstructUseTypeEnum.ignIt, - ), - ); - } - } - } - } - } - return uses; - } + /// Returns a list of [OneConstructUse] from itSteps + List get _itStepsToConstructUses => + originalSent?.choreo?.itStepsToConstructUses(event: event) ?? []; /// get construct uses of type vocab for the message List get _vocabUses { - final List uses = []; - - // missing vital info so return - if (event.roomId == null || originalSent?.tokens == null) { - // debugger(when: kDebugMode); - return uses; - } - - // for each token, record whether selected in ga, ta, or wa - for (final token in originalSent!.tokens! - .where((token) => token.lemma.saveVocab) - .toList()) { - uses.add(_getVocabUseForToken(token)); - } - - return uses; - } - - /// Returns a [OneConstructUse] for the given [token] - /// If there is no [originalSent] or [originalSent.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' - /// If the [token] is in the [originalSent.choreo.acceptedOrIgnoredMatch], - /// it is considered to be a [ConstructUseTypeEnum.ga]. - /// If the [token] is in the [originalSent.choreo.acceptedOrIgnoredMatch.choices], - /// it is considered to be a [ConstructUseTypeEnum.corIt]. - /// If the [token] is not included in any choreoStep, it is considered to be a [ConstructUseTypeEnum.wa]. - OneConstructUse _getVocabUseForToken(PangeaToken token) { - if (originalSent?.choreo == null) { - final bool inUserL2 = originalSent?.langCode == l2Code; - return _lemmaToVocabUse( - token.lemma, - inUserL2 ? ConstructUseTypeEnum.wa : ConstructUseTypeEnum.unk, + if (originalSent?.tokens != null) { + return originalSent!.content.vocabUses( + event: event, + choreo: originalSent!.choreo, + tokens: originalSent!.tokens!, ); } - - for (final step in originalSent!.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 - if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted && - (step.acceptedOrIgnoredMatch!.match.choices?.any( - (r) => - r.value.contains(token.text.content) && - step.text.contains(r.value), - ) ?? - false)) { - return _lemmaToVocabUse(token.lemma, ConstructUseTypeEnum.ga); - } - if (step.itStep != null) { - final bool pickedThroughIT = - step.itStep!.chosenContinuance?.text.contains(token.text.content) ?? - false; - if (pickedThroughIT) { - return _lemmaToVocabUse(token.lemma, ConstructUseTypeEnum.corIt); - //PTODO - check if added via custom input in IT flow - } - } - } - return _lemmaToVocabUse(token.lemma, ConstructUseTypeEnum.wa); + return []; } - OneConstructUse _lemmaToVocabUse( - Lemma lemma, - ConstructUseTypeEnum type, - ) => - OneConstructUse( - useType: type, - chatId: event.roomId!, - timeStamp: event.originServerTs, - lemma: lemma.text, - form: lemma.form, - msgId: event.eventId, - constructType: ConstructTypeEnum.vocab, - ); - /// get construct uses of type grammar for the message - List get _grammarConstructUses { - final List uses = []; - - if (originalSent?.choreo == null || event.roomId == null) return uses; - - for (final step in originalSent!.choreo!.choreoSteps) { - if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) { - final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ?? - step.acceptedOrIgnoredMatch!.match.shortMessage ?? - step.acceptedOrIgnoredMatch!.match.type.typeName.name; - uses.add( - OneConstructUse( - useType: ConstructUseTypeEnum.ga, - chatId: event.roomId!, - timeStamp: event.originServerTs, - lemma: name, - form: name, - msgId: event.eventId, - constructType: ConstructTypeEnum.grammar, - id: "${event.eventId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}", - ), - ); - } - } - return uses; - } + List get _grammarConstructUses => + originalSent?.choreo?.grammarConstructUses(event: event) ?? []; } class URLFinder { diff --git a/lib/pangea/matrix_event_wrappers/practice_activity_record_event.dart b/lib/pangea/matrix_event_wrappers/practice_activity_record_event.dart index 77b4948fd..d0f11da3c 100644 --- a/lib/pangea/matrix_event_wrappers/practice_activity_record_event.dart +++ b/lib/pangea/matrix_event_wrappers/practice_activity_record_event.dart @@ -72,9 +72,11 @@ class PracticeActivityRecordEvent { //TODO - find form of construct within the message //this is related to the feature of highlighting the target construct in the message form: construct.lemma, - chatId: event.roomId ?? practiceEvent.roomId ?? timeline.room.id, - msgId: practiceActivity.parentMessageId, - timeStamp: event.originServerTs, + metadata: ConstructUseMetaData( + roomId: event.roomId ?? practiceEvent.roomId ?? timeline.room.id, + eventId: practiceActivity.parentMessageId, + timeStamp: event.originServerTs, + ), ), ); } diff --git a/lib/pangea/models/analytics/constructs_model.dart b/lib/pangea/models/analytics/constructs_model.dart index ff6b55aef..4346ec1f1 100644 --- a/lib/pangea/models/analytics/constructs_model.dart +++ b/lib/pangea/models/analytics/constructs_model.dart @@ -37,12 +37,14 @@ class ConstructAnalyticsModel { for (final useData in lemmaUses) { final use = OneConstructUse( useType: ConstructUseTypeEnum.ga, - chatId: useData["chatId"], - timeStamp: DateTime.parse(useData["timeStamp"]), lemma: lemma, form: useData["form"], - msgId: useData["msgId"], constructType: ConstructTypeEnum.grammar, + metadata: ConstructUseMetaData( + eventId: useData["msgId"], + roomId: useData["chatId"], + timeStamp: DateTime.parse(useData["timeStamp"]), + ), ); uses.add(use); } @@ -69,71 +71,6 @@ class ConstructAnalyticsModel { } } -class OneConstructUse { - String? lemma; - ConstructTypeEnum? constructType; - String? form; - ConstructUseTypeEnum useType; - String chatId; - String? msgId; - DateTime timeStamp; - String? id; - - OneConstructUse({ - required this.useType, - required this.chatId, - required this.timeStamp, - required this.lemma, - required this.form, - required this.msgId, - required this.constructType, - this.id, - }); - - factory OneConstructUse.fromJson(Map json) { - return OneConstructUse( - useType: ConstructUseTypeEnum.values - .firstWhere((e) => e.string == json['useType']), - chatId: json['chatId'], - timeStamp: DateTime.parse(json['timeStamp']), - lemma: json['lemma'], - form: json['form'], - msgId: json['msgId'], - constructType: json['constructType'] != null - ? ConstructTypeUtil.fromString(json['constructType']) - : null, - id: json['id'], - ); - } - - Map toJson([bool condensed = false]) { - final Map data = { - 'useType': useType.string, - 'chatId': chatId, - 'timeStamp': timeStamp.toIso8601String(), - 'form': form, - 'msgId': msgId, - }; - if (!condensed && lemma != null) data['lemma'] = lemma!; - if (!condensed && constructType != null) { - data['constructType'] = constructType!.string; - } - if (id != null) data['id'] = id; - - return data; - } - - Room? getRoom(Client client) { - return client.getRoomById(chatId); - } - - Future getEvent(Client client) async { - final Room? room = getRoom(client); - if (room == null || msgId == null) return null; - return room.getEventById(msgId!); - } -} - class ConstructUses { final List uses; final ConstructTypeEnum constructType; @@ -145,3 +82,82 @@ class ConstructUses { required this.lemma, }); } + +class OneConstructUse { + String? lemma; + ConstructTypeEnum? constructType; + String? form; + ConstructUseTypeEnum useType; + String? id; + ConstructUseMetaData metadata; + + OneConstructUse({ + required this.useType, + required this.lemma, + required this.form, + required this.constructType, + required this.metadata, + this.id, + }); + + String get chatId => metadata.roomId; + String get msgId => metadata.eventId!; + DateTime get timeStamp => metadata.timeStamp; + + factory OneConstructUse.fromJson(Map json) { + return OneConstructUse( + useType: ConstructUseTypeEnum.values + .firstWhere((e) => e.string == json['useType']), + lemma: json['lemma'], + form: json['form'], + constructType: json['constructType'] != null + ? ConstructTypeUtil.fromString(json['constructType']) + : null, + id: json['id'], + metadata: ConstructUseMetaData( + eventId: json['msgId'], + roomId: json['chatId'], + timeStamp: DateTime.parse(json['timeStamp']), + ), + ); + } + + Map toJson([bool condensed = false]) { + final Map data = { + 'useType': useType.string, + 'chatId': metadata.roomId, + 'timeStamp': metadata.timeStamp.toIso8601String(), + 'form': form, + 'msgId': metadata.eventId, + }; + if (!condensed && lemma != null) data['lemma'] = lemma!; + if (!condensed && constructType != null) { + data['constructType'] = constructType!.string; + } + if (id != null) data['id'] = id; + + return data; + } + + Room? getRoom(Client client) { + return client.getRoomById(metadata.roomId); + } + + Future getEvent(Client client) async { + final Room? room = getRoom(client); + if (room == null || metadata.eventId == null) return null; + return room.getEventById(metadata.eventId!); + } +} + +class ConstructUseMetaData { + String? eventId; + String roomId; + DateTime timeStamp; + + ConstructUseMetaData({ + required this.roomId, + required this.timeStamp, + this.eventId, + }); +} diff --git a/lib/pangea/models/choreo_record.dart b/lib/pangea/models/choreo_record.dart index 3586fcee1..64ed4741f 100644 --- a/lib/pangea/models/choreo_record.dart +++ b/lib/pangea/models/choreo_record.dart @@ -1,6 +1,12 @@ import 'dart:convert'; +import 'package:fluffychat/pangea/constants/choreo_constants.dart'; +import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; +import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; +import 'package:matrix/matrix.dart'; import 'it_step.dart'; @@ -111,6 +117,100 @@ class ChoreoRecord { String get finalMessage => choreoSteps.isNotEmpty ? choreoSteps.last.text : ""; + + /// get construct uses of type grammar for the message + List grammarConstructUses({ + Event? event, + ConstructUseMetaData? metadata, + }) { + final List uses = []; + if (event?.roomId == null && metadata?.roomId == null) { + return uses; + } + metadata ??= ConstructUseMetaData( + roomId: event!.roomId!, + eventId: event.eventId, + timeStamp: event.originServerTs, + ); + + for (final step in choreoSteps) { + if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) { + final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ?? + step.acceptedOrIgnoredMatch!.match.shortMessage ?? + step.acceptedOrIgnoredMatch!.match.type.typeName.name; + uses.add( + OneConstructUse( + useType: ConstructUseTypeEnum.ga, + lemma: name, + form: name, + constructType: ConstructTypeEnum.grammar, + id: "${metadata.eventId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}", + metadata: metadata, + ), + ); + } + } + return uses; + } + + /// Returns a list of [OneConstructUse] from itSteps for which the continuance + /// was selected or ignored. Correct selections are considered in the tokens + /// flow. Once all continuances have lemmas, we can do both correct and incorrect + /// in this flow. It actually doesn't do anything at all right now, because the + /// choregrapher is not returning lemmas for continuances. This is a TODO. + /// So currently only the lemmas can be gotten from the tokens for choices that + /// are actually in the final message. + List itStepsToConstructUses({ + Event? event, + ConstructUseMetaData? metadata, + }) { + final List uses = []; + if (event == null && metadata == null) { + return uses; + } + + metadata ??= ConstructUseMetaData( + roomId: event!.roomId!, + eventId: event.eventId, + timeStamp: event.originServerTs, + ); + + for (final itStep in itSteps) { + for (final continuance in itStep.continuances) { + final List tokensToSave = + continuance.tokens.where((t) => t.lemma.saveVocab).toList(); + + if (finalMessage.contains(continuance.text)) { + continue; + } + if (continuance.wasClicked) { + //PTODO - account for end of flow score + if (continuance.level != ChoreoConstants.levelThresholdForGreen) { + for (final token in tokensToSave) { + uses.add( + token.lemma.toVocabUse( + ConstructUseTypeEnum.incIt, + metadata, + ), + ); + } + } + } else { + if (continuance.level != ChoreoConstants.levelThresholdForGreen) { + for (final token in tokensToSave) { + uses.add( + token.lemma.toVocabUse( + ConstructUseTypeEnum.ignIt, + metadata, + ), + ); + } + } + } + } + } + return uses; + } } /// A new ChoreoRecordStep is saved in the following cases: diff --git a/lib/pangea/models/lemma.dart b/lib/pangea/models/lemma.dart index 1dc44c4b5..f297d60b0 100644 --- a/lib/pangea/models/lemma.dart +++ b/lib/pangea/models/lemma.dart @@ -1,3 +1,7 @@ +import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; +import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; + /// Represents a lemma object class Lemma { /// [text] ex "ir" - text of the lemma of the word @@ -35,4 +39,18 @@ class Lemma { static Lemma create(String form) => Lemma(text: '', saveVocab: true, form: form); + + /// Given a [type] and [metadata], returns a [OneConstructUse] for this lemma + OneConstructUse toVocabUse( + ConstructUseTypeEnum type, + ConstructUseMetaData metadata, + ) { + return OneConstructUse( + useType: type, + lemma: text, + form: form, + constructType: ConstructTypeEnum.vocab, + metadata: metadata, + ); + } } diff --git a/lib/pangea/models/representation_content_model.dart b/lib/pangea/models/representation_content_model.dart index f49a465b4..3600267f3 100644 --- a/lib/pangea/models/representation_content_model.dart +++ b/lib/pangea/models/representation_content_model.dart @@ -1,4 +1,10 @@ +import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +import 'package:fluffychat/pangea/models/choreo_record.dart'; +import 'package:fluffychat/pangea/models/pangea_match_model.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:matrix/matrix.dart'; /// this class is contained within a [RepresentationEvent] @@ -81,4 +87,114 @@ class PangeaRepresentation { } return data; } + + /// Get construct uses of type vocab for the message. + /// Takes a list of tokens and a choreo record, which is searched + /// through for each token for its construct use type. + /// Also takes either an event (typically when the Representation itself is + /// available) or construct use metadata (when the event is not available, + /// i.e. immediately after message send) to create the construct use. + List vocabUses({ + required List tokens, + Event? event, + ConstructUseMetaData? metadata, + ChoreoRecord? choreo, + }) { + final List uses = []; + + // missing vital info so return + if (event?.roomId == null && metadata?.roomId == null) { + // debugger(when: kDebugMode); + return uses; + } + + metadata ??= ConstructUseMetaData( + roomId: event!.roomId!, + eventId: event.eventId, + timeStamp: event.originServerTs, + ); + + // for each token, record whether selected in ga, ta, or wa + final tokensToSave = + tokens.where((token) => token.lemma.saveVocab).toList(); + for (final token in tokensToSave) { + uses.add( + getVocabUseForToken( + token, + metadata, + choreo: choreo, + ), + ); + } + + return uses; + } + + /// 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' + /// If the [token] is in the [choreo.acceptedOrIgnoredMatch], it is considered to be a [ConstructUseTypeEnum.ga]. + /// If the [token] is in the [choreo.acceptedOrIgnoredMatch.choices], it is considered to be a [ConstructUseTypeEnum.corIt]. + /// If the [token] is not included in any choreoStep, it is considered to be a [ConstructUseTypeEnum.wa]. + OneConstructUse getVocabUseForToken( + PangeaToken token, + ConstructUseMetaData metadata, { + ChoreoRecord? choreo, + }) { + final lemma = token.lemma; + final content = token.text.content; + + if (choreo == null) { + final bool inUserL2 = langCode == + MatrixState.pangeaController.languageController.activeL2Code(); + return lemma.toVocabUse( + inUserL2 ? ConstructUseTypeEnum.wa : ConstructUseTypeEnum.unk, + metadata, + ); + } + + 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; + final bool isITStep = step.itStep != null; + if (!isAcceptedMatch && !isITStep) 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 the complete input text after this step + // contains the choice (why is this here?) + step.text.contains(choice.value), + ); + if (stepContainedToken) { + return lemma.toVocabUse( + ConstructUseTypeEnum.ga, + metadata, + ); + } + } + + if (isITStep && step.itStep?.chosenContinuance != null) { + final bool pickedThroughIT = + step.itStep!.chosenContinuance!.text.contains(content); + if (pickedThroughIT) { + return lemma.toVocabUse( + ConstructUseTypeEnum.corIt, + metadata, + ); + } + } + } + return lemma.toVocabUse( + ConstructUseTypeEnum.wa, + metadata, + ); + } }