moved functions for constructing constructs out of pangea message event so they can be created without direct access to the representation event

This commit is contained in:
ggurdin 2024-07-30 10:21:33 -04:00
parent bead112d0d
commit e7d176d182
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
6 changed files with 334 additions and 220 deletions

View file

@ -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<OneConstructUse> 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<OneConstructUse> get _itStepsToConstructUses {
final List<OneConstructUse> uses = [];
if (originalSent?.choreo == null) return uses;
for (final itStep in originalSent!.choreo!.itSteps) {
for (final continuance in itStep.continuances) {
final List<PangeaToken> 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<OneConstructUse> get _itStepsToConstructUses =>
originalSent?.choreo?.itStepsToConstructUses(event: event) ?? [];
/// get construct uses of type vocab for the message
List<OneConstructUse> get _vocabUses {
final List<OneConstructUse> 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<OneConstructUse> get _grammarConstructUses {
final List<OneConstructUse> 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<OneConstructUse> get _grammarConstructUses =>
originalSent?.choreo?.grammarConstructUses(event: event) ?? [];
}
class URLFinder {

View file

@ -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,
),
),
);
}

View file

@ -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<String, dynamic> 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<String, dynamic> toJson([bool condensed = false]) {
final Map<String, String?> 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<Event?> getEvent(Client client) async {
final Room? room = getRoom(client);
if (room == null || msgId == null) return null;
return room.getEventById(msgId!);
}
}
class ConstructUses {
final List<OneConstructUse> 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<String, dynamic> 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<String, dynamic> toJson([bool condensed = false]) {
final Map<String, String?> 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<Event?> 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,
});
}

View file

@ -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<OneConstructUse> grammarConstructUses({
Event? event,
ConstructUseMetaData? metadata,
}) {
final List<OneConstructUse> 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<OneConstructUse> itStepsToConstructUses({
Event? event,
ConstructUseMetaData? metadata,
}) {
final List<OneConstructUse> 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<PangeaToken> 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:

View file

@ -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,
);
}
}

View file

@ -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<OneConstructUse> vocabUses({
required List<PangeaToken> tokens,
Event? event,
ConstructUseMetaData? metadata,
ChoreoRecord? choreo,
}) {
final List<OneConstructUse> 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,
);
}
}