From d02acc988495f0e983bc7c72478018536e016cd1 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 3 Jul 2024 18:07:33 -0400 Subject: [PATCH 1/5] switching to token focus of saving constructs --- .../pangea_message_event.dart | 74 ++++++++++--------- lib/pangea/models/it_response_model.dart | 28 ++++--- lib/pangea/models/lemma.dart | 15 +--- lib/pangea/models/pangea_token_model.dart | 69 ++++++++--------- lib/pangea/repo/igc_repo.dart | 20 +++-- 5 files changed, 103 insertions(+), 103 deletions(-) diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 334c0fa78..0dfaa149a 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -695,21 +695,25 @@ class PangeaMessageEvent { if (continuance.wasClicked) { //PTODO - account for end of flow score if (continuance.level != ChoreoConstants.levelThresholdForGreen) { - uses.addAll( - _lemmasToVocabUses( - continuance.lemmas, - ConstructUseTypeEnum.incIt, - ), - ); + for (final token in continuance.tokens) { + uses.add( + _lemmaToVocabUse( + token.lemma, + ConstructUseTypeEnum.incIt, + ), + ); + } } } else { if (continuance.level != ChoreoConstants.levelThresholdForGreen) { - uses.addAll( - _lemmasToVocabUses( - continuance.lemmas, - ConstructUseTypeEnum.ignIt, - ), - ); + for (final token in continuance.tokens) { + uses.add( + _lemmaToVocabUse( + token.lemma, + ConstructUseTypeEnum.ignIt, + ), + ); + } } } } @@ -729,13 +733,13 @@ class PangeaMessageEvent { // for each token, record whether selected in ga, ta, or wa for (final token in originalSent!.tokens!) { - uses.addAll(_getVocabUseForToken(token)); + uses.add(_getVocabUseForToken(token)); } return uses; } - /// Returns a list of [OneConstructUse] objects for the given [token] + /// 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' @@ -744,11 +748,11 @@ class PangeaMessageEvent { /// 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]. - List _getVocabUseForToken(PangeaToken token) { + OneConstructUse _getVocabUseForToken(PangeaToken token) { if (originalSent?.choreo == null) { final bool inUserL2 = originalSent?.langCode == l2Code; - return _lemmasToVocabUses( - token.lemmas, + return _lemmaToVocabUse( + token.lemma, inUserL2 ? ConstructUseTypeEnum.wa : ConstructUseTypeEnum.unk, ); } @@ -763,42 +767,40 @@ class PangeaMessageEvent { step.text.contains(r.value), ) ?? false)) { - return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.ga); + 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 _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.corIt); + return _lemmaToVocabUse(token.lemma, ConstructUseTypeEnum.corIt); //PTODO - check if added via custom input in IT flow } } } - return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.wa); + return _lemmaToVocabUse(token.lemma, ConstructUseTypeEnum.wa); } /// Convert a list of [lemmas] into a list of vocab uses /// with the given [type] - List _lemmasToVocabUses( - List lemmas, + OneConstructUse _lemmaToVocabUse( + Lemma lemma, ConstructUseTypeEnum type, ) { final List uses = []; - for (final lemma in lemmas) { - if (lemma.saveVocab) { - uses.add( - OneConstructUse( - useType: type, - chatId: event.roomId!, - timeStamp: event.originServerTs, - lemma: lemma.text, - form: lemma.form, - msgId: event.eventId, - constructType: ConstructTypeEnum.vocab, - ), - ); - } + if (lemma.saveVocab) { + uses.add( + OneConstructUse( + useType: type, + chatId: event.roomId!, + timeStamp: event.originServerTs, + lemma: lemma.text, + form: lemma.form, + msgId: event.eventId, + constructType: ConstructTypeEnum.vocab, + ), + ); } return uses; } diff --git a/lib/pangea/models/it_response_model.dart b/lib/pangea/models/it_response_model.dart index 5fda36020..6adb4b4bc 100644 --- a/lib/pangea/models/it_response_model.dart +++ b/lib/pangea/models/it_response_model.dart @@ -2,11 +2,10 @@ 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/extensions/my_list_extension.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'lemma.dart'; - class ITResponseModel { String fullTextTranslation; List continuances; @@ -79,7 +78,7 @@ class Continuance { double probability; int level; String text; - List lemmas; + List tokens; /// saving this in a full json form String description; @@ -99,19 +98,18 @@ class Continuance { required this.inDictionary, required this.hasInfo, required this.gold, - required this.lemmas, + required this.tokens, }); factory Continuance.fromJson(Map json) { - final List lemmaInternal = - (json[ModelKey.lemma] != null && json[ModelKey.lemma] is Iterable) - ? (json[ModelKey.lemma] as Iterable) - .map( - (e) => Lemma.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, - lemmas: lemmaInternal, + tokens: tokensInternal, ); } @@ -132,7 +130,7 @@ class Continuance { data['level'] = level; data['text'] = text; data['clkd'] = wasClicked; - data[ModelKey.lemma] = lemmas.map((e) => e.toJson()).toList(); + data[ModelKey.tokens] = tokens.map((e) => e.toJson()).toList(); if (!condensed) { data['description'] = description; diff --git a/lib/pangea/models/lemma.dart b/lib/pangea/models/lemma.dart index 017a7ab88..1dc44c4b5 100644 --- a/lib/pangea/models/lemma.dart +++ b/lib/pangea/models/lemma.dart @@ -8,22 +8,13 @@ class Lemma { /// [saveVocab] true - whether to save the lemma to the user's vocabulary /// vocab that are not saved: emails, urls, numbers, punctuation, etc. + /// server handles this determination final bool saveVocab; - /// [pos] ex "v" - part of speech of the lemma - /// https://universaldependencies.org/u/pos/ - final String pos; - - /// [morph] ex {} - morphological features of the lemma - /// https://universaldependencies.org/u/feat/ - final Map morph; - Lemma({ required this.text, required this.saveVocab, required this.form, - this.pos = '', - this.morph = const {}, }); factory Lemma.fromJson(Map json) { @@ -31,8 +22,6 @@ class Lemma { text: json['text'], saveVocab: json['save_vocab'] ?? json['saveVocab'] ?? false, form: json["form"] ?? json['text'], - pos: json['pos'] ?? '', - morph: json['morph'] ?? '{}', ); } @@ -41,8 +30,6 @@ class Lemma { 'text': text, 'save_vocab': saveVocab, 'form': form, - 'pos': pos, - 'morph': morph, }; } diff --git a/lib/pangea/models/pangea_token_model.dart b/lib/pangea/models/pangea_token_model.dart index 9dddd149b..7055c29fa 100644 --- a/lib/pangea/models/pangea_token_model.dart +++ b/lib/pangea/models/pangea_token_model.dart @@ -1,55 +1,58 @@ import 'dart:developer'; import 'package:flutter/foundation.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; import '../constants/model_keys.dart'; -import '../utils/error_handler.dart'; import 'lemma.dart'; class PangeaToken { PangeaTokenText text; - List lemmas; + Lemma lemma; + + /// [pos] ex "VERB" - part of speech of the token + /// https://universaldependencies.org/u/pos/ + final String pos; + + /// [morph] ex {} - morphological features of the token + /// https://universaldependencies.org/u/feat/ + final Map morph; PangeaToken({ required this.text, - required this.lemmas, + required this.lemma, + required this.pos, + required this.morph, }); - static getLemmas(String text, Iterable? json) { + static _getLemmas(String text, dynamic json) { if (json != null) { - return json - .map( - (e) => Lemma.fromJson(e as Map), - ) - .toList() - .cast(); + // July 24, 2024 - we're changing from a list to a single lemma and this is for backwards compatibility + // previously sent tokens have lists of lemmas + if (json is Iterable) { + return json + .map( + (e) => Lemma.fromJson(e as Map), + ) + .toList() + .cast(); + } else { + return Lemma.fromJson(json); + } } else { - return [Lemma(text: text, saveVocab: false, form: text)]; + // earlier still, we didn't have lemmas so this is for really old tokens + return Lemma(text: text, saveVocab: false, form: text); } } factory PangeaToken.fromJson(Map json) { - try { - final PangeaTokenText text = - PangeaTokenText.fromJson(json[_textKey] as Map); - return PangeaToken( - text: text, - lemmas: getLemmas(text.content, json[_lemmaKey]), - ); - } catch (err, s) { - debugger(when: kDebugMode); - Sentry.addBreadcrumb( - Breadcrumb( - message: "PangeaToken.fromJson error", - data: { - "json": json, - }, - ), - ); - ErrorHandler.logError(e: err, s: s); - rethrow; - } + final PangeaTokenText text = + PangeaTokenText.fromJson(json[_textKey] as Map); + return PangeaToken( + text: text, + lemma: _getLemmas(text.content, json[_lemmaKey]), + pos: json['pos'] ?? '', + morph: json['morph'] ?? '{}', + ); } static const String _textKey = "text"; @@ -57,7 +60,7 @@ class PangeaToken { Map toJson() => { _textKey: text.toJson(), - _lemmaKey: lemmas.map((e) => e.toJson()).toList(), + _lemmaKey: lemma.toJson(), }; int get end => text.offset + text.length; diff --git a/lib/pangea/repo/igc_repo.dart b/lib/pangea/repo/igc_repo.dart index d6271470a..e32f6cab7 100644 --- a/lib/pangea/repo/igc_repo.dart +++ b/lib/pangea/repo/igc_repo.dart @@ -47,23 +47,33 @@ class IgcRepo { tokens: [ PangeaToken( text: PangeaTokenText(content: "This", offset: 0, length: 4), - lemmas: [Lemma(form: "This", text: "this", saveVocab: true)], + lemma: Lemma(form: "This", text: "this", saveVocab: true), + pos: "DET", + morph: {}, ), PangeaToken( text: PangeaTokenText(content: "be", offset: 5, length: 2), - lemmas: [Lemma(form: "be", text: "be", saveVocab: true)], + lemma: Lemma(form: "be", text: "be", saveVocab: true), + pos: "VERB", + morph: {}, ), PangeaToken( text: PangeaTokenText(content: "a", offset: 8, length: 1), - lemmas: [], + lemma: Lemma(form: "a", text: "a", saveVocab: true), + pos: "DET", + morph: {}, ), PangeaToken( text: PangeaTokenText(content: "sample", offset: 10, length: 6), - lemmas: [], + lemma: Lemma(form: "sample", text: "sample", saveVocab: true), + pos: "NOUN", + morph: {}, ), PangeaToken( text: PangeaTokenText(content: "text", offset: 17, length: 4), - lemmas: [], + lemma: Lemma(form: "text", text: "text", saveVocab: true), + pos: "NOUN", + morph: {}, ), ], matches: [ From 77df13f5a9c4ef8e511390c9407ff66e9032383e Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 3 Jul 2024 18:10:49 -0400 Subject: [PATCH 2/5] --amend --- .../pangea_message_event.dart | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 0dfaa149a..530a7a547 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -687,7 +687,8 @@ class PangeaMessageEvent { for (final itStep in originalSent!.choreo!.itSteps) { for (final continuance in itStep.continuances) { - // this seems to always be false for continuances right now + final List tokensToSave = + continuance.tokens.where((t) => t.lemma.saveVocab).toList(); if (originalSent!.choreo!.finalMessage.contains(continuance.text)) { continue; @@ -695,7 +696,7 @@ class PangeaMessageEvent { if (continuance.wasClicked) { //PTODO - account for end of flow score if (continuance.level != ChoreoConstants.levelThresholdForGreen) { - for (final token in continuance.tokens) { + for (final token in tokensToSave) { uses.add( _lemmaToVocabUse( token.lemma, @@ -706,7 +707,7 @@ class PangeaMessageEvent { } } else { if (continuance.level != ChoreoConstants.levelThresholdForGreen) { - for (final token in continuance.tokens) { + for (final token in tokensToSave) { uses.add( _lemmaToVocabUse( token.lemma, @@ -732,7 +733,9 @@ class PangeaMessageEvent { } // for each token, record whether selected in ga, ta, or wa - for (final token in originalSent!.tokens!) { + for (final token in originalSent!.tokens! + .where((token) => token.lemma.saveVocab) + .toList()) { uses.add(_getVocabUseForToken(token)); } @@ -782,28 +785,19 @@ class PangeaMessageEvent { return _lemmaToVocabUse(token.lemma, ConstructUseTypeEnum.wa); } - /// Convert a list of [lemmas] into a list of vocab uses - /// with the given [type] OneConstructUse _lemmaToVocabUse( Lemma lemma, ConstructUseTypeEnum type, - ) { - final List uses = []; - if (lemma.saveVocab) { - uses.add( - OneConstructUse( - useType: type, - chatId: event.roomId!, - timeStamp: event.originServerTs, - lemma: lemma.text, - form: lemma.form, - msgId: event.eventId, - constructType: ConstructTypeEnum.vocab, - ), + ) => + OneConstructUse( + useType: type, + chatId: event.roomId!, + timeStamp: event.originServerTs, + lemma: lemma.text, + form: lemma.form, + msgId: event.eventId, + constructType: ConstructTypeEnum.vocab, ); - } - return uses; - } /// get construct uses of type grammar for the message List get _grammarConstructUses { From bca1b87677aa62cce8d730cadd012b21c6d48dd7 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Sun, 7 Jul 2024 19:31:29 -0400 Subject: [PATCH 3/5] added span display details to activity --- .../controllers/language_list_controller.dart | 1 - .../activity_display_instructions_enum.dart | 13 +++++ lib/pangea/models/pangea_token_model.dart | 16 ++++-- .../multiple_choice_activity_model.dart | 7 +++ .../practice_activity_model.dart | 56 +++++++++++++++++++ 5 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 lib/pangea/enum/activity_display_instructions_enum.dart diff --git a/lib/pangea/controllers/language_list_controller.dart b/lib/pangea/controllers/language_list_controller.dart index 31e4513fa..77f4ae9e2 100644 --- a/lib/pangea/controllers/language_list_controller.dart +++ b/lib/pangea/controllers/language_list_controller.dart @@ -93,7 +93,6 @@ class PangeaLanguage { } static LanguageModel byLangCode(String langCode) { - final list = _langList; for (final element in _langList) { if (element.langCode == langCode) return element; } diff --git a/lib/pangea/enum/activity_display_instructions_enum.dart b/lib/pangea/enum/activity_display_instructions_enum.dart new file mode 100644 index 000000000..9a96d669c --- /dev/null +++ b/lib/pangea/enum/activity_display_instructions_enum.dart @@ -0,0 +1,13 @@ +enum ActivityDisplayInstructionsEnum { highlight, hide } + +extension ActivityDisplayInstructionsEnumExt + on ActivityDisplayInstructionsEnum { + String get string { + switch (this) { + case ActivityDisplayInstructionsEnum.highlight: + return 'highlight'; + case ActivityDisplayInstructionsEnum.hide: + return 'hide'; + } + } +} diff --git a/lib/pangea/models/pangea_token_model.dart b/lib/pangea/models/pangea_token_model.dart index 7055c29fa..608d5b9d1 100644 --- a/lib/pangea/models/pangea_token_model.dart +++ b/lib/pangea/models/pangea_token_model.dart @@ -24,17 +24,19 @@ class PangeaToken { required this.morph, }); - static _getLemmas(String text, dynamic json) { + static Lemma _getLemmas(String text, dynamic json) { if (json != null) { // July 24, 2024 - we're changing from a list to a single lemma and this is for backwards compatibility // previously sent tokens have lists of lemmas if (json is Iterable) { return json - .map( - (e) => Lemma.fromJson(e as Map), - ) - .toList() - .cast(); + .map( + (e) => Lemma.fromJson(e as Map), + ) + .toList() + .cast() + .firstOrNull ?? + Lemma(text: text, saveVocab: false, form: text); } else { return Lemma.fromJson(json); } @@ -61,6 +63,8 @@ class PangeaToken { Map toJson() => { _textKey: text.toJson(), _lemmaKey: lemma.toJson(), + 'pos': pos, + 'morph': morph, }; int get end => text.offset + text.length; diff --git a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart index 68376d410..18302bd43 100644 --- a/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart @@ -1,15 +1,18 @@ import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:flutter/material.dart'; class MultipleChoice { final String question; final List choices; final String answer; + final RelevantSpanDisplayDetails? spanDisplayDetails; MultipleChoice({ required this.question, required this.choices, required this.answer, + this.spanDisplayDetails, }); bool isCorrect(int index) => index == correctAnswerIndex; @@ -28,6 +31,9 @@ class MultipleChoice { question: json['question'] as String, choices: (json['choices'] as List).map((e) => e as String).toList(), answer: json['answer'] ?? json['correct_answer'] as String, + spanDisplayDetails: json['span_display_details'] != null + ? RelevantSpanDisplayDetails.fromJson(json['span_display_details']) + : null, ); } @@ -36,6 +42,7 @@ class MultipleChoice { 'question': question, 'choices': choices, 'answer': answer, + 'span_display_details': spanDisplayDetails, }; } } diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart index fa3e25acf..645d550e5 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -1,5 +1,7 @@ import 'dart:developer'; +import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/enum/activity_display_instructions_enum.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart'; @@ -279,4 +281,58 @@ class PracticeActivityModel { 'free_response': freeResponse?.toJson(), }; } + + RelevantSpanDisplayDetails? getRelevantSpanDisplayDetails() { + switch (activityType) { + case ActivityTypeEnum.multipleChoice: + return multipleChoice?.spanDisplayDetails; + case ActivityTypeEnum.listening: + return null; + case ActivityTypeEnum.speaking: + return null; + case ActivityTypeEnum.freeResponse: + return null; + default: + debugger(when: kDebugMode); + return null; + } + } +} + +/// For those activities with a relevant span, this class will hold the details +/// of the span and how it should be displayed +/// e.g. hide the span for conjugation activities +class RelevantSpanDisplayDetails { + final int offset; + final int length; + final ActivityDisplayInstructionsEnum displayInstructions; + + RelevantSpanDisplayDetails({ + required this.offset, + required this.length, + required this.displayInstructions, + }); + + factory RelevantSpanDisplayDetails.fromJson(Map json) { + final ActivityDisplayInstructionsEnum? display = + ActivityDisplayInstructionsEnum.values.firstWhereOrNull( + (e) => e.string == json['display_instructions'], + ); + if (display == null) { + debugger(when: kDebugMode); + } + return RelevantSpanDisplayDetails( + offset: json['offset'] as int, + length: json['length'] as int, + displayInstructions: display ?? ActivityDisplayInstructionsEnum.hide, + ); + } + + Map toJson() { + return { + 'offset': offset, + 'length': length, + 'display_instructions': displayInstructions, + }; + } } From 0ac4b664cee36b701f2b469d3d67cda908a5aef3 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 12 Jul 2024 17:03:00 -0400 Subject: [PATCH 4/5] changed from string to Map --- lib/pangea/models/pangea_token_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pangea/models/pangea_token_model.dart b/lib/pangea/models/pangea_token_model.dart index 608d5b9d1..ba0772740 100644 --- a/lib/pangea/models/pangea_token_model.dart +++ b/lib/pangea/models/pangea_token_model.dart @@ -53,7 +53,7 @@ class PangeaToken { text: text, lemma: _getLemmas(text.content, json[_lemmaKey]), pos: json['pos'] ?? '', - morph: json['morph'] ?? '{}', + morph: json['morph'] ?? {}, ); } From c4ef72857888adc4842d2b09d6123e31f4c484c4 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Tue, 16 Jul 2024 09:06:58 -0400 Subject: [PATCH 5/5] client will consume lemma as single object but be capable of handling json lemma as list --- lib/pangea/models/pangea_token_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pangea/models/pangea_token_model.dart b/lib/pangea/models/pangea_token_model.dart index ba0772740..089521e1a 100644 --- a/lib/pangea/models/pangea_token_model.dart +++ b/lib/pangea/models/pangea_token_model.dart @@ -62,7 +62,7 @@ class PangeaToken { Map toJson() => { _textKey: text.toJson(), - _lemmaKey: lemma.toJson(), + _lemmaKey: [lemma.toJson()], 'pos': pos, 'morph': morph, };