update activity models to reduce duplicate data
This commit is contained in:
parent
45c31afc2b
commit
af92158fa1
23 changed files with 353 additions and 383 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -38,9 +38,7 @@ class AnalyticsPracticeSessionModel {
|
|||
userL1: userL1,
|
||||
userL2: userL2,
|
||||
activityQualityFeedback: null,
|
||||
targetTokens: target.tokens,
|
||||
targetType: target.activityType,
|
||||
targetMorphFeature: target.morphFeature,
|
||||
target: target,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue