update activity models to reduce duplicate data

This commit is contained in:
ggurdin 2026-01-15 12:47:44 -05:00
parent 45c31afc2b
commit af92158fa1
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
23 changed files with 353 additions and 383 deletions

View file

@ -17,12 +17,11 @@ import 'package:fluffychat/pangea/analytics_practice/analytics_practice_view.dar
import 'package:fluffychat/pangea/common/utils/async_state.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_generation_repo.dart';
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/practice_record_controller.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -74,8 +73,8 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
final ValueNotifier<double> progressNotifier = ValueNotifier<double>(0.0);
final Map<PracticeTarget, Map<String, String>> _choiceTexts = {};
final Map<PracticeTarget, Map<String, String?>> _choiceEmojis = {};
final Map<String, Map<String, String>> _choiceTexts = {};
final Map<String, Map<String, String?>> _choiceEmojis = {};
StreamSubscription<void>? _languageStreamSubscription;
@ -120,16 +119,16 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
Matrix.of(context).analyticsDataService;
List<PracticeChoice> filteredChoices(
PracticeTarget target,
MultipleChoiceActivity activity,
MultipleChoicePracticeActivityModel activity,
) {
final choices = activity.choices.toList();
final answer = activity.answers.first;
final content = activity.multipleChoiceContent;
final choices = content.choices.toList();
final answer = content.answers.first;
final filtered = <PracticeChoice>[];
final seenTexts = <String>{};
for (final id in choices) {
final text = getChoiceText(target, id);
final text = getChoiceText(activity.storageKey, id);
if (seenTexts.contains(text)) {
if (id != answer) {
@ -143,7 +142,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
filtered[index] = PracticeChoice(
choiceId: id,
choiceText: text,
choiceEmoji: getChoiceEmoji(target, id),
choiceEmoji: getChoiceEmoji(activity.storageKey, id),
);
}
continue;
@ -154,7 +153,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
PracticeChoice(
choiceId: id,
choiceText: text,
choiceEmoji: getChoiceEmoji(target, id),
choiceEmoji: getChoiceEmoji(activity.storageKey, id),
),
);
}
@ -162,21 +161,21 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
return filtered;
}
String getChoiceText(PracticeTarget target, String choiceId) {
String getChoiceText(String key, String choiceId) {
if (widget.type == ConstructTypeEnum.morph) {
return choiceId;
}
if (_choiceTexts.containsKey(target) &&
_choiceTexts[target]!.containsKey(choiceId)) {
return _choiceTexts[target]![choiceId]!;
if (_choiceTexts.containsKey(key) &&
_choiceTexts[key]!.containsKey(choiceId)) {
return _choiceTexts[key]![choiceId]!;
}
final cId = ConstructIdentifier.fromString(choiceId);
return cId?.lemma ?? choiceId;
}
String? getChoiceEmoji(PracticeTarget target, String choiceId) {
String? getChoiceEmoji(String key, String choiceId) {
if (widget.type == ConstructTypeEnum.morph) return null;
return _choiceEmojis[target]?[choiceId];
return _choiceEmojis[key]?[choiceId];
}
String choiceTargetId(String choiceId) =>
@ -297,7 +296,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
final res = await _fetchActivity(req);
if (!mounted) return;
activityTarget.value = req.practiceTarget;
activityTarget.value = req.target;
activityState.value = AsyncState.loaded(res);
} catch (e) {
if (!mounted) return;
@ -311,12 +310,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
Future<void> _fillActivityQueue(List<MessageActivityRequest> requests) async {
for (final request in requests) {
final completer = Completer<MultipleChoicePracticeActivityModel>();
_queue.add(
MapEntry(
request.practiceTarget,
completer,
),
);
_queue.add(MapEntry(request.target, completer));
try {
final res = await _fetchActivity(request);
@ -346,16 +340,16 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
final activityModel = result.result as MultipleChoicePracticeActivityModel;
// Prefetch lemma info for meaning activities before marking ready
if (activityModel.activityType == ActivityTypeEnum.lemmaMeaning) {
if (activityModel is VocabMeaningPracticeActivityModel) {
final choices = activityModel.multipleChoiceContent.choices.toList();
await _fetchLemmaInfo(activityModel.practiceTarget, choices);
await _fetchLemmaInfo(activityModel.storageKey, choices);
}
return activityModel;
}
Future<void> _fetchLemmaInfo(
PracticeTarget target,
String requestKey,
List<String> choiceIds,
) async {
final texts = <String, String>{};
@ -375,22 +369,25 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
emojis[id] = res.result!.emoji.firstOrNull;
}
_choiceTexts.putIfAbsent(target, () => {});
_choiceEmojis.putIfAbsent(target, () => {});
_choiceTexts.putIfAbsent(requestKey, () => {});
_choiceEmojis.putIfAbsent(requestKey, () => {});
_choiceTexts[target]!.addAll(texts);
_choiceEmojis[target]!.addAll(emojis);
_choiceTexts[requestKey]!.addAll(texts);
_choiceEmojis[requestKey]!.addAll(emojis);
}
Future<void> onSelectChoice(
ConstructIdentifier choiceConstruct,
String choiceContent,
) async {
if (_currentActivity == null) return;
final activity = _currentActivity!;
// Update activity record
activity.onMultipleChoiceSelect(choiceConstruct, choiceContent);
PracticeRecordController.onSelectChoice(
choiceContent,
activity.tokens.first,
activity,
);
final use = activity.constructUse(choiceContent);
_sessionLoader.value!.submitAnswer(use);

View file

@ -38,9 +38,7 @@ class AnalyticsPracticeSessionModel {
userL1: userL1,
userL2: userL2,
activityQualityFeedback: null,
targetTokens: target.tokens,
targetType: target.activityType,
targetMorphFeature: target.morphFeature,
target: target,
);
}).toList();
}

View file

@ -236,10 +236,7 @@ class _ActivityChoicesWidget extends StatelessWidget {
AsyncLoaded<MultipleChoicePracticeActivityModel>(:final value) =>
LayoutBuilder(
builder: (context, constraints) {
final choices = controller.filteredChoices(
value.practiceTarget,
value.multipleChoiceContent,
);
final choices = controller.filteredChoices(value);
final constrainedHeight =
constraints.maxHeight.clamp(0.0, 400.0);
final cardHeight = (constrainedHeight / (choices.length + 1))
@ -258,7 +255,6 @@ class _ActivityChoicesWidget extends StatelessWidget {
controller.choiceTargetId(choice.choiceId),
choiceId: choice.choiceId,
onPressed: () => controller.onSelectChoice(
value.targetTokens.first.vocabConstructID,
choice.choiceId,
),
cardHeight: cardHeight,
@ -307,7 +303,7 @@ class _ChoiceCard extends StatelessWidget {
Widget build(BuildContext context) {
final isCorrect = activity.multipleChoiceContent.isCorrect(choiceId);
final activityType = activity.activityType;
final constructId = activity.targetTokens.first.vocabConstructID;
final constructId = activity.tokens.first.vocabConstructID;
switch (activity.activityType) {
case ActivityTypeEnum.lemmaMeaning:

View file

@ -11,14 +11,14 @@ class MorphCategoryActivityGenerator {
static Future<MessageActivityResponse> get(
MessageActivityRequest req,
) async {
if (req.targetMorphFeature == null) {
if (req.target.morphFeature == null) {
throw ArgumentError(
"MorphCategoryActivityGenerator requires a targetMorphFeature",
);
}
final feature = req.targetMorphFeature!;
final morphTag = req.targetTokens.first.getMorphTag(feature);
final feature = req.target.morphFeature!;
final morphTag = req.target.tokens.first.getMorphTag(feature);
if (morphTag == null) {
throw ArgumentError(
"Token does not have the specified morph feature",
@ -51,7 +51,7 @@ class MorphCategoryActivityGenerator {
return MessageActivityResponse(
activity: MorphCategoryPracticeActivityModel(
targetTokens: [req.targetTokens.first],
tokens: req.target.tokens,
langCode: req.userL2,
morphFeature: feature,
multipleChoiceContent: MultipleChoiceActivity(

View file

@ -7,7 +7,7 @@ class VocabAudioActivityGenerator {
static Future<MessageActivityResponse> get(
MessageActivityRequest req,
) async {
final token = req.targetTokens.first;
final token = req.target.tokens.first;
final choices =
await LemmaActivityGenerator.lemmaActivityDistractors(token);
@ -16,7 +16,7 @@ class VocabAudioActivityGenerator {
return MessageActivityResponse(
activity: VocabAudioPracticeActivityModel(
targetTokens: [token],
tokens: req.target.tokens,
langCode: req.userL2,
multipleChoiceContent: MultipleChoiceActivity(
choices: choicesList.toSet(),

View file

@ -7,7 +7,7 @@ class VocabMeaningActivityGenerator {
static Future<MessageActivityResponse> get(
MessageActivityRequest req,
) async {
final token = req.targetTokens.first;
final token = req.target.tokens.first;
final choices =
await LemmaActivityGenerator.lemmaActivityDistractors(token);
@ -19,7 +19,7 @@ class VocabMeaningActivityGenerator {
return MessageActivityResponse(
activity: VocabMeaningPracticeActivityModel(
targetTokens: [token],
tokens: req.target.tokens,
langCode: req.userL2,
multipleChoiceContent: MultipleChoiceActivity(
choices: constructIdChoices,

View file

@ -228,12 +228,12 @@ enum ActivityTypeEnum {
ActivityTypeEnum.morphId,
];
static List<ActivityTypeEnum> get vocabPracticeTypes => [
static List<ActivityTypeEnum> get _vocabPracticeTypes => [
ActivityTypeEnum.lemmaMeaning,
// ActivityTypeEnum.lemmaAudio,
];
static List<ActivityTypeEnum> get grammarPracticeTypes => [
static List<ActivityTypeEnum> get _grammarPracticeTypes => [
ActivityTypeEnum.grammarCategory,
];
@ -242,26 +242,9 @@ enum ActivityTypeEnum {
) {
switch (constructType) {
case ConstructTypeEnum.vocab:
return vocabPracticeTypes;
return _vocabPracticeTypes;
case ConstructTypeEnum.morph:
return grammarPracticeTypes;
}
}
ConstructTypeEnum get constructType {
switch (this) {
case ActivityTypeEnum.wordMeaning:
case ActivityTypeEnum.wordFocusListening:
case ActivityTypeEnum.hiddenWordListening:
case ActivityTypeEnum.lemmaId:
case ActivityTypeEnum.emoji:
case ActivityTypeEnum.messageMeaning:
case ActivityTypeEnum.lemmaMeaning:
case ActivityTypeEnum.lemmaAudio:
return ConstructTypeEnum.vocab;
case ActivityTypeEnum.morphId:
case ActivityTypeEnum.grammarCategory:
return ConstructTypeEnum.morph;
return _grammarPracticeTypes;
}
}
}

View file

@ -12,7 +12,7 @@ class EmojiActivityGenerator {
MessageActivityRequest req, {
required Map<String, dynamic> messageInfo,
}) async {
if (req.targetTokens.length <= 1) {
if (req.target.tokens.length <= 1) {
throw Exception("Emoji activity requires at least 2 tokens");
}
@ -27,7 +27,7 @@ class EmojiActivityGenerator {
final List<PangeaToken> missingEmojis = [];
final List<String> usedEmojis = [];
for (final token in req.targetTokens) {
for (final token in req.target.tokens) {
final userSavedEmoji = token.vocabConstructID.userSetEmoji;
if (userSavedEmoji != null && !usedEmojis.contains(userSavedEmoji)) {
matchInfo[token.vocabForm] = [userSavedEmoji];
@ -65,7 +65,7 @@ class EmojiActivityGenerator {
return MessageActivityResponse(
activity: EmojiPracticeActivityModel(
targetTokens: req.targetTokens,
tokens: req.target.tokens,
langCode: req.userL2,
matchContent: PracticeMatchActivity(
matchInfo: matchInfo,

View file

@ -14,15 +14,15 @@ class LemmaActivityGenerator {
static Future<MessageActivityResponse> get(
MessageActivityRequest req,
) async {
debugger(when: kDebugMode && req.targetTokens.length != 1);
debugger(when: kDebugMode && req.target.tokens.length != 1);
final token = req.targetTokens.first;
final token = req.target.tokens.first;
final choices = await lemmaActivityDistractors(token);
// TODO - modify MultipleChoiceActivity flow to allow no correct answer
return MessageActivityResponse(
activity: LemmaPracticeActivityModel(
targetTokens: [token],
tokens: req.target.tokens,
langCode: req.userL2,
multipleChoiceContent: MultipleChoiceActivity(
choices: choices.map((c) => c.lemma).toSet(),

View file

@ -15,7 +15,7 @@ class LemmaMeaningActivityGenerator {
required Map<String, dynamic> messageInfo,
}) async {
final List<Future<Result<LemmaInfoResponse>>> lemmaInfoFutures = req
.targetTokens
.target.tokens
.map((token) => token.vocabConstructID.getLemmaInfo(messageInfo))
.toList();
@ -27,13 +27,13 @@ class LemmaMeaningActivityGenerator {
}
final Map<ConstructForm, List<String>> matchInfo = Map.fromIterables(
req.targetTokens.map((token) => token.vocabForm),
req.target.tokens.map((token) => token.vocabForm),
lemmaInfos.map((lemmaInfo) => [lemmaInfo.asValue!.value.meaning]),
);
return MessageActivityResponse(
activity: LemmaMeaningPracticeActivityModel(
targetTokens: req.targetTokens,
tokens: req.target.tokens,
langCode: req.userL2,
matchContent: PracticeMatchActivity(
matchInfo: matchInfo,

View file

@ -1,9 +1,5 @@
import 'package:collection/collection.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
@ -42,70 +38,49 @@ class ActivityQualityFeedback {
class MessageActivityRequest {
final String userL1;
final String userL2;
final List<PangeaToken> targetTokens;
final ActivityTypeEnum targetType;
final MorphFeaturesEnum? targetMorphFeature;
final PracticeTarget target;
final ActivityQualityFeedback? activityQualityFeedback;
MessageActivityRequest({
required this.userL1,
required this.userL2,
required this.activityQualityFeedback,
required this.targetTokens,
required this.targetType,
required this.targetMorphFeature,
required this.target,
}) {
if (targetTokens.isEmpty) {
if (target.tokens.isEmpty) {
throw Exception('Target tokens must not be empty');
}
}
String get activityText {
switch (targetType) {
case ActivityTypeEnum.grammarCategory:
return "${targetTokens.first.vocabConstructID.lemma}: ${targetMorphFeature!.name}";
default:
return targetTokens.first.vocabConstructID.lemma;
}
}
Map<String, dynamic> toJson() {
return {
'user_l1': userL1,
'user_l2': userL2,
'activity_quality_feedback': activityQualityFeedback?.toJson(),
'target_tokens': targetTokens.map((e) => e.toJson()).toList(),
'target_type': targetType.name,
'target_morph_feature': targetMorphFeature,
'target_tokens': target.tokens.map((e) => e.toJson()).toList(),
'target_type': target.activityType.name,
'target_morph_feature': target.morphFeature,
};
}
PracticeTarget get practiceTarget => PracticeTarget(
activityType: targetType,
tokens: targetTokens,
morphFeature: targetMorphFeature,
);
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is MessageActivityRequest &&
other.targetType == targetType &&
other.userL1 == userL1 &&
other.userL2 == userL2 &&
other.target == target &&
other.activityQualityFeedback?.feedbackText ==
activityQualityFeedback?.feedbackText &&
const ListEquality().equals(other.targetTokens, targetTokens) &&
other.targetMorphFeature == targetMorphFeature;
activityQualityFeedback?.feedbackText;
}
@override
int get hashCode {
return targetType.hashCode ^
activityQualityFeedback.hashCode ^
targetTokens.hashCode ^
targetMorphFeature.hashCode;
return activityQualityFeedback.hashCode ^
target.hashCode ^
userL1.hashCode ^
userL2.hashCode;
}
}

View file

@ -17,13 +17,13 @@ class MorphActivityGenerator {
static MessageActivityResponse get(
MessageActivityRequest req,
) {
debugger(when: kDebugMode && req.targetTokens.length != 1);
debugger(when: kDebugMode && req.target.tokens.length != 1);
debugger(when: kDebugMode && req.targetMorphFeature == null);
debugger(when: kDebugMode && req.target.morphFeature == null);
final PangeaToken token = req.targetTokens.first;
final PangeaToken token = req.target.tokens.first;
final MorphFeaturesEnum morphFeature = req.targetMorphFeature!;
final MorphFeaturesEnum morphFeature = req.target.morphFeature!;
final String? morphTag = token.getMorphTag(morphFeature);
if (morphTag == null) {
@ -38,7 +38,7 @@ class MorphActivityGenerator {
return MessageActivityResponse(
activity: MorphMatchPracticeActivityModel(
targetTokens: req.targetTokens,
tokens: req.target.tokens,
langCode: req.userL2,
morphFeature: morphFeature,
multipleChoiceContent: MultipleChoiceActivity(

View file

@ -3,31 +3,54 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.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/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
import 'package:fluffychat/pangea/practice_activities/practice_match.dart';
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
sealed class PracticeActivityModel {
final List<PangeaToken> targetTokens;
final ActivityTypeEnum activityType;
final List<PangeaToken> tokens;
final String langCode;
const PracticeActivityModel({
required this.targetTokens,
required this.tokens,
required this.langCode,
required this.activityType,
});
String get storageKey =>
'${activityType.name}-${tokens.map((e) => e.text.content).join("-")}';
PracticeTarget get practiceTarget => PracticeTarget(
tokens: targetTokens,
activityType: activityType,
tokens: tokens,
morphFeature: this is MorphPracticeActivityModel
? (this as MorphPracticeActivityModel).morphFeature
: null,
);
ActivityTypeEnum get activityType {
switch (this) {
case MorphCategoryPracticeActivityModel():
return ActivityTypeEnum.grammarCategory;
case VocabAudioPracticeActivityModel():
return ActivityTypeEnum.lemmaAudio;
case VocabMeaningPracticeActivityModel():
return ActivityTypeEnum.lemmaMeaning;
case EmojiPracticeActivityModel():
return ActivityTypeEnum.emoji;
case LemmaPracticeActivityModel():
return ActivityTypeEnum.lemmaId;
case LemmaMeaningPracticeActivityModel():
return ActivityTypeEnum.wordMeaning;
case MorphMatchPracticeActivityModel():
return ActivityTypeEnum.morphId;
case WordListeningPracticeActivityModel():
return ActivityTypeEnum.wordFocusListening;
}
}
factory PracticeActivityModel.fromJson(Map<String, dynamic> json) {
if (json['lang_code'] is! String) {
Sentry.addBreadcrumb(
@ -83,7 +106,7 @@ sealed class PracticeActivityModel {
);
return MorphCategoryPracticeActivityModel(
langCode: langCode,
targetTokens: tokens,
tokens: tokens,
morphFeature: morph!,
multipleChoiceContent: multipleChoiceContent!,
);
@ -94,7 +117,7 @@ sealed class PracticeActivityModel {
);
return VocabAudioPracticeActivityModel(
langCode: langCode,
targetTokens: tokens,
tokens: tokens,
multipleChoiceContent: multipleChoiceContent!,
);
case ActivityTypeEnum.lemmaMeaning:
@ -104,7 +127,7 @@ sealed class PracticeActivityModel {
);
return VocabMeaningPracticeActivityModel(
langCode: langCode,
targetTokens: tokens,
tokens: tokens,
multipleChoiceContent: multipleChoiceContent!,
);
case ActivityTypeEnum.emoji:
@ -114,7 +137,7 @@ sealed class PracticeActivityModel {
);
return EmojiPracticeActivityModel(
langCode: langCode,
targetTokens: tokens,
tokens: tokens,
matchContent: matchContent!,
);
case ActivityTypeEnum.lemmaId:
@ -124,7 +147,7 @@ sealed class PracticeActivityModel {
);
return LemmaPracticeActivityModel(
langCode: langCode,
targetTokens: tokens,
tokens: tokens,
multipleChoiceContent: multipleChoiceContent!,
);
case ActivityTypeEnum.wordMeaning:
@ -134,7 +157,7 @@ sealed class PracticeActivityModel {
);
return LemmaMeaningPracticeActivityModel(
langCode: langCode,
targetTokens: tokens,
tokens: tokens,
matchContent: matchContent!,
);
case ActivityTypeEnum.morphId:
@ -148,7 +171,7 @@ sealed class PracticeActivityModel {
);
return MorphMatchPracticeActivityModel(
langCode: langCode,
targetTokens: tokens,
tokens: tokens,
morphFeature: morph!,
multipleChoiceContent: multipleChoiceContent!,
);
@ -159,7 +182,7 @@ sealed class PracticeActivityModel {
);
return WordListeningPracticeActivityModel(
langCode: langCode,
targetTokens: tokens,
tokens: tokens,
matchContent: matchContent!,
);
default:
@ -171,7 +194,7 @@ sealed class PracticeActivityModel {
return {
'lang_code': langCode,
'activity_type': activityType.name,
'target_tokens': targetTokens.map((e) => e.toJson()).toList(),
'target_tokens': tokens.map((e) => e.toJson()).toList(),
};
}
}
@ -180,40 +203,18 @@ sealed class MultipleChoicePracticeActivityModel extends PracticeActivityModel {
final MultipleChoiceActivity multipleChoiceContent;
MultipleChoicePracticeActivityModel({
required super.targetTokens,
required super.tokens,
required super.langCode,
required super.activityType,
required this.multipleChoiceContent,
});
bool onMultipleChoiceSelect(
ConstructIdentifier choiceConstruct,
String choice,
) {
if (practiceTarget.isComplete ||
practiceTarget.record.alreadyHasMatchResponse(
choiceConstruct,
choice,
)) {
// the user has already selected this choice
// so we don't want to record it again
return false;
}
final bool isCorrect = multipleChoiceContent.isCorrect(choice);
practiceTarget.record.addResponse(
cId: choiceConstruct,
target: practiceTarget,
text: choice,
score: isCorrect ? 1 : 0,
);
return isCorrect;
}
bool isCorrect(String choice) => multipleChoiceContent.isCorrect(choice);
OneConstructUse constructUse(String choiceContent) {
final correct = multipleChoiceContent.isCorrect(choiceContent);
final useType =
correct ? activityType.correctUse : activityType.incorrectUse;
final token = tokens.first;
return OneConstructUse(
useType: useType,
@ -222,9 +223,9 @@ sealed class MultipleChoicePracticeActivityModel extends PracticeActivityModel {
roomId: null,
timeStamp: DateTime.now(),
),
category: targetTokens.first.pos,
lemma: targetTokens.first.lemma.text,
form: targetTokens.first.lemma.text,
category: token.pos,
lemma: token.lemma.text,
form: token.lemma.text,
xp: useType.pointValue,
);
}
@ -241,37 +242,16 @@ sealed class MatchPracticeActivityModel extends PracticeActivityModel {
final PracticeMatchActivity matchContent;
MatchPracticeActivityModel({
required super.targetTokens,
required super.tokens,
required super.langCode,
required super.activityType,
required this.matchContent,
});
bool onMatch(
bool isCorrect(
PangeaToken token,
PracticeChoice choice,
) {
// the user has already selected this choice
// so we don't want to record it again
if (practiceTarget.isComplete ||
practiceTarget.record.alreadyHasMatchResponse(
token.vocabConstructID,
choice.choiceContent,
)) {
return false;
}
final answers = matchContent.matchInfo[token.vocabForm];
final isCorrect = answers!.contains(choice.choiceContent);
practiceTarget.record.addResponse(
cId: token.vocabConstructID,
target: practiceTarget,
text: choice.choiceContent,
score: isCorrect ? 1 : 0,
);
return isCorrect;
}
String choice,
) =>
matchContent.matchInfo[token.vocabForm]!.contains(choice);
@override
Map<String, dynamic> toJson() {
@ -286,19 +266,15 @@ sealed class MorphPracticeActivityModel
final MorphFeaturesEnum morphFeature;
MorphPracticeActivityModel({
required super.targetTokens,
required super.tokens,
required super.langCode,
required super.activityType,
required super.multipleChoiceContent,
required this.morphFeature,
});
@override
PracticeTarget get practiceTarget => PracticeTarget(
tokens: targetTokens,
activityType: activityType,
morphFeature: morphFeature,
);
String get storageKey =>
'${activityType.name}-${tokens.map((e) => e.text.content).join("-")}-${morphFeature.name}';
@override
Map<String, dynamic> toJson() {
@ -310,20 +286,19 @@ sealed class MorphPracticeActivityModel
class MorphCategoryPracticeActivityModel extends MorphPracticeActivityModel {
MorphCategoryPracticeActivityModel({
required super.targetTokens,
required super.tokens,
required super.langCode,
required super.morphFeature,
required super.multipleChoiceContent,
}) : super(
activityType: ActivityTypeEnum.grammarCategory,
);
});
@override
OneConstructUse constructUse(String choiceContent) {
final correct = multipleChoiceContent.isCorrect(choiceContent);
final token = tokens.first;
final useType =
correct ? activityType.correctUse : activityType.incorrectUse;
final tag = targetTokens.first.getMorphTag(morphFeature)!;
final tag = token.getMorphTag(morphFeature)!;
return OneConstructUse(
useType: useType,
@ -334,7 +309,7 @@ class MorphCategoryPracticeActivityModel extends MorphPracticeActivityModel {
),
category: morphFeature.name,
lemma: tag,
form: targetTokens.first.lemma.form,
form: token.lemma.form,
xp: useType.pointValue,
);
}
@ -342,73 +317,59 @@ class MorphCategoryPracticeActivityModel extends MorphPracticeActivityModel {
class MorphMatchPracticeActivityModel extends MorphPracticeActivityModel {
MorphMatchPracticeActivityModel({
required super.targetTokens,
required super.tokens,
required super.langCode,
required super.morphFeature,
required super.multipleChoiceContent,
}) : super(
activityType: ActivityTypeEnum.morphId,
);
});
}
class VocabAudioPracticeActivityModel
extends MultipleChoicePracticeActivityModel {
VocabAudioPracticeActivityModel({
required super.targetTokens,
required super.tokens,
required super.langCode,
required super.multipleChoiceContent,
}) : super(
activityType: ActivityTypeEnum.lemmaAudio,
);
});
}
class VocabMeaningPracticeActivityModel
extends MultipleChoicePracticeActivityModel {
VocabMeaningPracticeActivityModel({
required super.targetTokens,
required super.tokens,
required super.langCode,
required super.multipleChoiceContent,
}) : super(
activityType: ActivityTypeEnum.lemmaMeaning,
);
});
}
class LemmaPracticeActivityModel extends MultipleChoicePracticeActivityModel {
LemmaPracticeActivityModel({
required super.targetTokens,
required super.tokens,
required super.langCode,
required super.multipleChoiceContent,
}) : super(
activityType: ActivityTypeEnum.lemmaId,
);
});
}
class EmojiPracticeActivityModel extends MatchPracticeActivityModel {
EmojiPracticeActivityModel({
required super.targetTokens,
required super.tokens,
required super.langCode,
required super.matchContent,
}) : super(
activityType: ActivityTypeEnum.emoji,
);
});
}
class LemmaMeaningPracticeActivityModel extends MatchPracticeActivityModel {
LemmaMeaningPracticeActivityModel({
required super.targetTokens,
required super.tokens,
required super.langCode,
required super.matchContent,
}) : super(
activityType: ActivityTypeEnum.wordMeaning,
);
});
}
class WordListeningPracticeActivityModel extends MatchPracticeActivityModel {
WordListeningPracticeActivityModel({
required super.targetTokens,
required super.tokens,
required super.langCode,
required super.matchContent,
}) : super(
activityType: ActivityTypeEnum.wordFocusListening,
);
});
}

View file

@ -117,7 +117,7 @@ class PracticeRepo {
required Map<String, dynamic> messageInfo,
}) async {
// some activities we'll get from the server and others we'll generate locally
switch (req.targetType) {
switch (req.target.activityType) {
case ActivityTypeEnum.emoji:
return EmojiActivityGenerator.get(req, messageInfo: messageInfo);
case ActivityTypeEnum.lemmaId:

View file

@ -1,19 +1,12 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
import 'package:fluffychat/pangea/practice_activities/practice_record.dart';
import 'package:fluffychat/pangea/practice_activities/practice_record_repo.dart';
/// Picks which tokens to do activities on and what types of activities to do
/// Caches result so that we don't have to recompute it
@ -91,82 +84,6 @@ class PracticeTarget {
(morphFeature?.name ?? "");
}
PracticeRecord get record => PracticeRecordRepo.get(this);
bool get isComplete {
if (activityType == ActivityTypeEnum.morphId) {
return record.completeResponses > 0;
}
return tokens.every(
(t) => record.responses
.any((res) => res.cId == t.vocabConstructID && res.isCorrect),
);
}
bool isCompleteByToken(PangeaToken token, [MorphFeaturesEnum? morph]) {
final ConstructIdentifier? cId =
morph == null ? token.vocabConstructID : token.morphIdByFeature(morph);
if (cId == null) {
debugger(when: kDebugMode);
ErrorHandler.logError(
m: "isCompleteByToken: cId is null for token ${token.text.content}",
data: {
"t": token.toJson(),
"morph": morph?.name,
},
);
return false;
}
if (activityType == ActivityTypeEnum.morphId) {
return record.responses.any(
(res) => res.cId == token.morphIdByFeature(morph!) && res.isCorrect,
);
}
return record.responses.any(
(res) => res.cId == token.vocabConstructID && res.isCorrect,
);
}
bool? wasCorrectChoice(String choice) {
for (final response in record.responses) {
if (response.text == choice) {
return response.isCorrect;
}
}
return null;
}
/// if any of the choices were correct, return true
/// if all of the choices were incorrect, return false
/// if null, it means the user has not yet responded with that choice
bool? wasCorrectMatch(PracticeChoice choice) {
for (final response in record.responses) {
if (response.text == choice.choiceContent && response.isCorrect) {
return true;
}
}
for (final response in record.responses) {
if (response.text == choice.choiceContent) {
return false;
}
}
return null;
}
bool get hasAnyResponses => record.responses.isNotEmpty;
bool get hasAnyCorrectChoices {
for (final response in record.responses) {
if (response.isCorrect) {
return true;
}
}
return false;
}
String promptText(BuildContext context) {
switch (activityType) {
case ActivityTypeEnum.grammarCategory:

View file

@ -7,7 +7,7 @@ class WordFocusListeningGenerator {
static MessageActivityResponse get(
MessageActivityRequest req,
) {
if (req.targetTokens.length <= 1) {
if (req.target.tokens.length <= 1) {
throw Exception(
"Word focus listening activity requires at least 2 tokens",
);
@ -15,11 +15,11 @@ class WordFocusListeningGenerator {
return MessageActivityResponse(
activity: WordListeningPracticeActivityModel(
targetTokens: req.targetTokens,
tokens: req.target.tokens,
langCode: req.userL2,
matchContent: PracticeMatchActivity(
matchInfo: Map.fromEntries(
req.targetTokens.map(
req.target.tokens.map(
(token) => MapEntry(
ConstructForm(
cId: token.vocabConstructID,

View file

@ -482,7 +482,7 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
final type =
practice.practiceMode.associatedActivityType;
final complete = type != null &&
practice.isPracticeActivityDone(type);
practice.isPracticeSessionDone(type);
if (instruction != null && !complete) {
return InstructionsInlineTooltip(

View file

@ -51,7 +51,7 @@ class MessageMorphInputBarContentState
extends State<MessageMorphInputBarContent> {
String? selectedTag;
PangeaToken get token => widget.activity.targetTokens.first;
PangeaToken get token => widget.activity.tokens.first;
MorphFeaturesEnum get morph => widget.activity.morphFeature;
@override
@ -116,8 +116,7 @@ class MessageMorphInputBarContentState
runSpacing: spacing,
children: widget.activity.multipleChoiceContent.choices.mapIndexed(
(index, choice) {
final wasCorrect =
widget.activity.practiceTarget.wasCorrectChoice(choice);
final wasCorrect = widget.controller.wasCorrectChoice(choice);
return ChoiceAnimationWidget(
isSelected: selectedTag == choice,
@ -135,8 +134,7 @@ class MessageMorphInputBarContentState
PracticeChoice(
choiceContent: choice,
form: ConstructForm(
cId: widget.activity.targetTokens.first
.morphIdByFeature(
cId: widget.activity.tokens.first.morphIdByFeature(
widget.activity.morphFeature,
)!,
form: token.text.content,

View file

@ -1,4 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:async/async.dart';
import 'package:collection/collection.dart';
@ -18,6 +18,7 @@ import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.da
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/message_practice_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/morph_selection.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/practice_record_controller.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -37,14 +38,43 @@ class PracticeController with ChangeNotifier {
PracticeSelection? practiceSelection;
bool get isTotallyDone =>
isPracticeActivityDone(ActivityTypeEnum.emoji) &&
isPracticeActivityDone(ActivityTypeEnum.wordMeaning) &&
isPracticeActivityDone(ActivityTypeEnum.wordFocusListening) &&
isPracticeActivityDone(ActivityTypeEnum.morphId);
bool? wasCorrectMatch(PracticeChoice choice) {
if (_activity == null) return false;
final record = PracticeRecordController.recordByActivity(_activity!);
for (final response in record.responses) {
if (response.text == choice.choiceContent && response.isCorrect) {
return true;
}
}
for (final response in record.responses) {
if (response.text == choice.choiceContent) {
return false;
}
}
return null;
}
bool isPracticeActivityDone(ActivityTypeEnum activityType) =>
practiceSelection?.activities(activityType).every((a) => a.isComplete) ==
bool? wasCorrectChoice(String choice) {
if (_activity == null) return false;
final record = PracticeRecordController.recordByActivity(_activity!);
for (final response in record.responses) {
if (response.text == choice) {
return response.isCorrect;
}
}
return null;
}
bool get isTotallyDone =>
isPracticeSessionDone(ActivityTypeEnum.emoji) &&
isPracticeSessionDone(ActivityTypeEnum.wordMeaning) &&
isPracticeSessionDone(ActivityTypeEnum.wordFocusListening) &&
isPracticeSessionDone(ActivityTypeEnum.morphId);
bool isPracticeSessionDone(ActivityTypeEnum activityType) =>
practiceSelection
?.activities(activityType)
.every((a) => PracticeRecordController.isCompleteByTarget(a)) ==
true;
bool isPracticeButtonEmpty(PangeaToken token) {
@ -67,21 +97,38 @@ class PracticeController with ChangeNotifier {
? (_activity as MorphPracticeActivityModel).morphFeature
: null;
return target == null || target.isCompleteByToken(token, morph) == true;
return target == null ||
PracticeRecordController.isCompleteByToken(
target,
token,
morph,
);
}
bool get showChoiceShimmer {
if (_activity == null) return false;
final record = PracticeRecordController.recordByActivity(_activity!);
if (_activity!.activityType == ActivityTypeEnum.morphId) {
return selectedMorph != null &&
!_activity!.practiceTarget.hasAnyResponses;
if (_activity is MorphMatchPracticeActivityModel) {
return selectedMorph != null && record.responses.isEmpty;
}
return selectedChoice == null &&
!_activity!.practiceTarget.hasAnyCorrectChoices;
return selectedChoice == null && !record.responses.any((r) => r.isCorrect);
}
// PracticeTarget? get _target => _activity != null
// ? PracticeTarget(
// activityType: _activity!.activityType,
// tokens: _activity!.tokens,
// morphFeature: _activity is MorphPracticeActivityModel
// ? (_activity as MorphPracticeActivityModel).morphFeature
// : null,
// )
// : null;
// PracticeRecord? get _record =>
// _target != null ? PracticeRecordRepo.get(_target!) : null;
Future<void> _fetchPracticeSelection() async {
if (pangeaMessageEvent.messageDisplayRepresentation?.tokens == null) return;
practiceSelection = await PracticeSelectionRepo.get(
@ -98,9 +145,7 @@ class PracticeController with ChangeNotifier {
userL1: MatrixState.pangeaController.userController.userL1!.langCode,
userL2: MatrixState.pangeaController.userController.userL2!.langCode,
activityQualityFeedback: null,
targetTokens: target.tokens,
targetType: target.activityType,
targetMorphFeature: target.morphFeature,
target: target,
);
final result = await PracticeRepo.getPracticeActivity(
@ -148,13 +193,12 @@ class PracticeController with ChangeNotifier {
void onMatch(PangeaToken token, PracticeChoice choice) {
if (_activity == null) return;
final isCorrect = switch (_activity!) {
MultipleChoicePracticeActivityModel() =>
(_activity as MultipleChoicePracticeActivityModel)
.onMultipleChoiceSelect(choice.form.cId, choice.choiceContent),
MatchPracticeActivityModel() =>
(_activity as MatchPracticeActivityModel).onMatch(token, choice),
};
final record = PracticeRecordController.recordByActivity(_activity!);
final isCorrect = PracticeRecordController.onSelectChoice(
choice.choiceContent,
token,
_activity!,
);
final targetId =
"message-token-${token.text.uniqueKey}-${pangeaMessageEvent.eventId}";
@ -163,9 +207,9 @@ class PracticeController with ChangeNotifier {
.pangeaController.matrixState.analyticsDataService.updateService;
// we don't take off points for incorrect emoji matches
if (_activity!.activityType != ActivityTypeEnum.emoji || isCorrect) {
final constructUseType = _activity!.practiceTarget.record.responses.last
.useType(_activity!.activityType);
if (_activity is! EmojiPracticeActivityModel || isCorrect) {
final constructUseType =
record.responses.last.useType(_activity!.activityType);
final constructs = [
OneConstructUse(
@ -191,14 +235,14 @@ class PracticeController with ChangeNotifier {
}
if (isCorrect) {
if (_activity!.activityType == ActivityTypeEnum.emoji) {
if (_activity is EmojiPracticeActivityModel) {
updateService.setLemmaInfo(
choice.form.cId,
emoji: choice.choiceContent,
);
}
if (_activity!.activityType == ActivityTypeEnum.wordMeaning) {
if (_activity is LemmaMeaningPracticeActivityModel) {
updateService.setLemmaInfo(
choice.form.cId,
meaning: choice.choiceContent,

View file

@ -1,13 +1,9 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/common/widgets/choice_animation.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/message_audio_card.dart';
@ -25,16 +21,16 @@ class MatchActivityCard extends StatelessWidget {
required this.controller,
});
ActivityTypeEnum get activityType => currentActivity.activityType;
// ActivityTypeEnum get activityType => currentActivity.activityType;
Widget choiceDisplayContent(
BuildContext context,
String choice,
double? fontSize,
) {
switch (activityType) {
case ActivityTypeEnum.emoji:
case ActivityTypeEnum.wordMeaning:
switch (currentActivity) {
case EmojiPracticeActivityModel():
case LemmaMeaningPracticeActivityModel():
return Padding(
padding: const EdgeInsets.all(8),
child: Text(
@ -43,7 +39,7 @@ class MatchActivityCard extends StatelessWidget {
textAlign: TextAlign.center,
),
);
case ActivityTypeEnum.wordFocusListening:
case WordListeningPracticeActivityModel():
return Padding(
padding: const EdgeInsets.all(8),
child: Icon(
@ -51,9 +47,6 @@ class MatchActivityCard extends StatelessWidget {
size: fontSize,
),
);
default:
debugger(when: kDebugMode);
return const SizedBox();
}
}
@ -83,13 +76,12 @@ class MatchActivityCard extends StatelessWidget {
runSpacing: 4.0,
children: currentActivity.matchContent.choices.map(
(PracticeChoice cf) {
final bool? wasCorrect =
currentActivity.practiceTarget.wasCorrectMatch(cf);
final bool? wasCorrect = controller.wasCorrectMatch(cf);
return ChoiceAnimationWidget(
isSelected: controller.selectedChoice == cf,
isCorrect: wasCorrect,
child: PracticeMatchItem(
token: currentActivity.practiceTarget.tokens.firstWhereOrNull(
token: currentActivity.tokens.firstWhereOrNull(
(t) => t.vocabConstructID == cf.form.cId,
),
isSelected: controller.selectedChoice == cf,
@ -98,7 +90,7 @@ class MatchActivityCard extends StatelessWidget {
content:
choiceDisplayContent(context, cf.choiceContent, fontSize),
audioContent:
activityType == ActivityTypeEnum.wordFocusListening
currentActivity is WordListeningPracticeActivityModel
? cf.choiceContent
: null,
controller: controller,

View file

@ -0,0 +1,108 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
import 'package:fluffychat/pangea/practice_activities/practice_record.dart';
import 'package:fluffychat/pangea/practice_activities/practice_record_repo.dart';
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
class PracticeRecordController {
static PracticeRecord recordByActivity(PracticeActivityModel activity) =>
PracticeRecordRepo.get(activity.practiceTarget);
static bool isCompleteByTarget(PracticeTarget target) {
final record = PracticeRecordRepo.get(target);
if (target.activityType == ActivityTypeEnum.morphId) {
return record.completeResponses > 0;
}
return target.tokens.every(
(t) => record.responses
.any((res) => res.cId == t.vocabConstructID && res.isCorrect),
);
}
static bool isCompleteByActivity(PracticeActivityModel activity) {
final activityRecord = recordByActivity(activity);
if (activity.activityType == ActivityTypeEnum.morphId) {
return activityRecord.completeResponses > 0;
}
return activity.tokens.every(
(t) => activityRecord.responses
.any((res) => res.cId == t.vocabConstructID && res.isCorrect),
);
}
static bool isCompleteByToken(
PracticeTarget target,
PangeaToken token, [
MorphFeaturesEnum? morph,
]) {
final ConstructIdentifier? cId =
morph == null ? token.vocabConstructID : token.morphIdByFeature(morph);
if (cId == null) {
debugger(when: kDebugMode);
ErrorHandler.logError(
m: "isCompleteByToken: cId is null for token ${token.text.content}",
data: {
"t": token.toJson(),
"morph": morph?.name,
},
);
return false;
}
final record = PracticeRecordRepo.get(target);
if (target.activityType == ActivityTypeEnum.morphId) {
return record.responses.any(
(res) => res.cId == token.morphIdByFeature(morph!) && res.isCorrect,
);
}
return record.responses.any(
(res) => res.cId == token.vocabConstructID && res.isCorrect,
);
}
static bool hasAnyCorrectChoices(PracticeTarget target) {
final record = PracticeRecordRepo.get(target);
return record.responses.any((response) => response.isCorrect);
}
static bool onSelectChoice(
String choice,
PangeaToken token,
PracticeActivityModel activity,
) {
final record = recordByActivity(activity);
if (isCompleteByActivity(activity) ||
record.alreadyHasMatchResponse(
token.vocabConstructID,
choice,
)) {
return false;
}
final isCorrect = switch (activity) {
MatchPracticeActivityModel() => activity.isCorrect(token, choice),
MultipleChoicePracticeActivityModel() => activity.isCorrect(choice),
};
record.addResponse(
cId: token.vocabConstructID,
target: activity.practiceTarget,
text: choice,
score: isCorrect ? 1 : 0,
);
return isCorrect;
}
}

View file

@ -54,7 +54,7 @@ class ReadingAssistanceInputBarState extends State<ReadingAssistanceInputBar> {
children: [
...MessagePracticeMode.practiceModes.map(
(m) {
final complete = widget.controller.isPracticeActivityDone(
final complete = widget.controller.isPracticeSessionDone(
m.associatedActivityType!,
);
return ToolbarButton(
@ -125,7 +125,7 @@ class _ReadingAssistanceBarContent extends StatelessWidget {
}
final activityType = mode.associatedActivityType;
final activityCompleted =
activityType != null && controller.isPracticeActivityDone(activityType);
activityType != null && controller.isPracticeSessionDone(activityType);
switch (mode) {
case MessagePracticeMode.noneSelected:

View file

@ -14,11 +14,13 @@ import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
import 'package:fluffychat/pangea/practice_activities/practice_choice.dart';
import 'package:fluffychat/pangea/practice_activities/practice_record_repo.dart';
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/dotted_border_painter.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/message_practice_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/morph_selection.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/practice_controller.dart';
import 'package:fluffychat/pangea/toolbar/message_practice/practice_record_controller.dart';
import 'package:fluffychat/widgets/hover_builder.dart';
const double tokenButtonHeight = 40.0;
@ -48,11 +50,8 @@ class TokenPracticeButton extends StatelessWidget {
PracticeTarget? get _activity => controller.practiceTargetForToken(token);
bool get isActivityCompleteOrNullForToken {
return _activity?.isCompleteByToken(
token,
_activity!.morphFeature,
) ==
true;
if (_activity == null) return true;
return PracticeRecordController.isCompleteByToken(_activity!, token);
}
bool get _isEmpty => controller.isPracticeButtonEmpty(token);
@ -94,7 +93,8 @@ class TokenPracticeButton extends StatelessWidget {
),
),
shimmer: controller.selectedMorph == null &&
_activity?.hasAnyCorrectChoices == false,
_activity != null &&
!PracticeRecordController.hasAnyCorrectChoices(_activity!),
);
} else {
child = _StandardMatchButton(
@ -257,8 +257,9 @@ class _NoActivityContentButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (practiceMode == MessagePracticeMode.wordEmoji) {
final displayEmoji = target?.record.responses
if (practiceMode == MessagePracticeMode.wordEmoji && target != null) {
final record = PracticeRecordRepo.get(target!);
final displayEmoji = record.responses
.firstWhereOrNull(
(res) => res.cId == token.vocabConstructID && res.isCorrect,
)