fix example messages for grammar activities, make practice activity model a sealed class
This commit is contained in:
parent
b698e2e84f
commit
326e5c3241
18 changed files with 486 additions and 328 deletions
|
|
@ -10,9 +10,12 @@ import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dar
|
|||
class ExampleMessageUtil {
|
||||
static Future<List<InlineSpan>?> getExampleMessage(
|
||||
ConstructUses construct,
|
||||
Client client,
|
||||
) async {
|
||||
Client client, {
|
||||
String? form,
|
||||
}) async {
|
||||
for (final use in construct.cappedUses) {
|
||||
if (form != null && use.form != form) continue;
|
||||
|
||||
final event = await client.getEventByConstructUse(use);
|
||||
if (event == null) continue;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import 'package:fluffychat/pangea/analytics_data/analytics_data_service.dart';
|
|||
import 'package:fluffychat/pangea/analytics_data/analytics_updater_mixin.dart';
|
||||
import 'package:fluffychat/pangea/analytics_data/derived_analytics_data_model.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/analytics_misc/example_message_util.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_repo.dart';
|
||||
|
|
@ -64,13 +62,15 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
with AnalyticsUpdater {
|
||||
late final SessionLoader _sessionLoader;
|
||||
|
||||
final ValueNotifier<AsyncState<PracticeActivityModel>> activityState =
|
||||
ValueNotifier(const AsyncState.idle());
|
||||
final ValueNotifier<AsyncState<MultipleChoicePracticeActivityModel>>
|
||||
activityState = ValueNotifier(const AsyncState.idle());
|
||||
|
||||
final Queue<MapEntry<String, Completer<PracticeActivityModel>>> _queue =
|
||||
Queue();
|
||||
final Queue<
|
||||
MapEntry<PracticeTarget,
|
||||
Completer<MultipleChoicePracticeActivityModel>>> _queue = Queue();
|
||||
|
||||
final ValueNotifier<String?> activityText = ValueNotifier<String?>(null);
|
||||
final ValueNotifier<PracticeTarget?> activityTarget =
|
||||
ValueNotifier<PracticeTarget?>(null);
|
||||
|
||||
final ValueNotifier<double> progressNotifier = ValueNotifier<double>(0.0);
|
||||
|
||||
|
|
@ -99,14 +99,16 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
}
|
||||
_sessionLoader.dispose();
|
||||
activityState.dispose();
|
||||
activityText.dispose();
|
||||
activityTarget.dispose();
|
||||
progressNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
PracticeActivityModel? get _currentActivity =>
|
||||
activityState.value is AsyncLoaded<PracticeActivityModel>
|
||||
? (activityState.value as AsyncLoaded<PracticeActivityModel>).value
|
||||
MultipleChoicePracticeActivityModel? get _currentActivity =>
|
||||
activityState.value is AsyncLoaded<MultipleChoicePracticeActivityModel>
|
||||
? (activityState.value
|
||||
as AsyncLoaded<MultipleChoicePracticeActivityModel>)
|
||||
.value
|
||||
: null;
|
||||
|
||||
bool get _isComplete => _sessionLoader.value?.isComplete ?? false;
|
||||
|
|
@ -177,7 +179,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
|
||||
void _resetActivityState() {
|
||||
activityState.value = const AsyncState.loading();
|
||||
activityText.value = null;
|
||||
activityTarget.value = null;
|
||||
}
|
||||
|
||||
void _resetSessionState() {
|
||||
|
|
@ -258,14 +260,15 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
_continuing = true;
|
||||
|
||||
try {
|
||||
if (activityState.value is AsyncIdle<PracticeActivityModel>) {
|
||||
if (activityState.value
|
||||
is AsyncIdle<MultipleChoicePracticeActivityModel>) {
|
||||
await _initActivityData();
|
||||
} else if (_queue.isEmpty) {
|
||||
await _completeSession();
|
||||
} else {
|
||||
activityState.value = const AsyncState.loading();
|
||||
final nextActivityCompleter = _queue.removeFirst();
|
||||
activityText.value = nextActivityCompleter.key;
|
||||
activityTarget.value = nextActivityCompleter.key;
|
||||
final activity = await nextActivityCompleter.value.future;
|
||||
activityState.value = AsyncState.loaded(activity);
|
||||
}
|
||||
|
|
@ -289,7 +292,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
final res = await _fetchActivity(req);
|
||||
if (!mounted) return;
|
||||
|
||||
activityText.value = req.activityText;
|
||||
activityTarget.value = req.practiceTarget;
|
||||
activityState.value = AsyncState.loaded(res);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
|
|
@ -302,13 +305,14 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
|
||||
Future<void> _fillActivityQueue(List<MessageActivityRequest> requests) async {
|
||||
for (final request in requests) {
|
||||
final completer = Completer<PracticeActivityModel>();
|
||||
final completer = Completer<MultipleChoicePracticeActivityModel>();
|
||||
_queue.add(
|
||||
MapEntry(
|
||||
request.activityText,
|
||||
request.practiceTarget,
|
||||
completer,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final res = await _fetchActivity(request);
|
||||
if (!mounted) return;
|
||||
|
|
@ -321,24 +325,28 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
}
|
||||
}
|
||||
|
||||
Future<PracticeActivityModel> _fetchActivity(
|
||||
Future<MultipleChoicePracticeActivityModel> _fetchActivity(
|
||||
MessageActivityRequest req,
|
||||
) async {
|
||||
final result = await PracticeRepo.getPracticeActivity(
|
||||
req,
|
||||
messageInfo: {},
|
||||
);
|
||||
if (result.isError) {
|
||||
|
||||
if (result.isError ||
|
||||
result.result is! MultipleChoicePracticeActivityModel) {
|
||||
throw L10n.of(context).oopsSomethingWentWrong;
|
||||
}
|
||||
|
||||
final activityModel = result.result as MultipleChoicePracticeActivityModel;
|
||||
|
||||
// Prefetch lemma info for meaning activities before marking ready
|
||||
if (result.result!.activityType == ActivityTypeEnum.lemmaMeaning) {
|
||||
final choices = result.result!.multipleChoiceContent!.choices.toList();
|
||||
await _fetchLemmaInfo(result.result!.practiceTarget, choices);
|
||||
if (activityModel.activityType == ActivityTypeEnum.lemmaMeaning) {
|
||||
final choices = activityModel.multipleChoiceContent.choices.toList();
|
||||
await _fetchLemmaInfo(activityModel.practiceTarget, choices);
|
||||
}
|
||||
|
||||
return result.result!;
|
||||
return activityModel;
|
||||
}
|
||||
|
||||
Future<void> _fetchLemmaInfo(
|
||||
|
|
@ -378,32 +386,14 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
|
||||
// Update activity record
|
||||
activity.onMultipleChoiceSelect(choiceConstruct, choiceContent);
|
||||
final correct = activity.multipleChoiceContent!.isCorrect(choiceContent);
|
||||
|
||||
// Update session model and analytics
|
||||
final useType = correct
|
||||
? activity.activityType.correctUse
|
||||
: activity.activityType.incorrectUse;
|
||||
|
||||
final use = OneConstructUse(
|
||||
useType: useType,
|
||||
constructType: widget.type,
|
||||
metadata: ConstructUseMetaData(
|
||||
roomId: null,
|
||||
timeStamp: DateTime.now(),
|
||||
),
|
||||
category: activity.useCategory,
|
||||
lemma: activity.useLemma,
|
||||
form: activity.useForm,
|
||||
xp: useType.pointValue,
|
||||
);
|
||||
|
||||
final use = activity.constructUse(choiceContent);
|
||||
_sessionLoader.value!.submitAnswer(use);
|
||||
await _analyticsService.updateService
|
||||
.addAnalytics(choiceTargetId(choiceContent), [use]);
|
||||
|
||||
await _saveSession();
|
||||
if (!correct) return;
|
||||
if (!activity.multipleChoiceContent.isCorrect(choiceContent)) return;
|
||||
|
||||
// Display the fact that the choice was correct before loading the next activity
|
||||
await Future.delayed(const Duration(milliseconds: 1000));
|
||||
|
|
@ -417,11 +407,26 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
|
|||
}
|
||||
|
||||
Future<List<InlineSpan>?> getExampleMessage(
|
||||
ConstructIdentifier construct,
|
||||
PracticeTarget target,
|
||||
) async {
|
||||
final token = target.tokens.first;
|
||||
final construct = switch (widget.type) {
|
||||
ConstructTypeEnum.vocab => token.vocabConstructID,
|
||||
ConstructTypeEnum.morph => token.morphIdByFeature(target.morphFeature!),
|
||||
};
|
||||
|
||||
if (construct == null) return null;
|
||||
|
||||
String? form;
|
||||
if (widget.type == ConstructTypeEnum.morph) {
|
||||
if (target.morphFeature == null) return null;
|
||||
form = token.lemma.form;
|
||||
}
|
||||
|
||||
return ExampleMessageUtil.getExampleMessage(
|
||||
await _analyticsService.getConstructUse(construct),
|
||||
Matrix.of(context).client,
|
||||
form: form,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -119,10 +119,10 @@ class _AnalyticsActivityView extends StatelessWidget {
|
|||
Expanded(
|
||||
flex: 1,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller.activityText,
|
||||
builder: (context, text, __) => text != null
|
||||
valueListenable: controller.activityTarget,
|
||||
builder: (context, target, __) => target != null
|
||||
? Text(
|
||||
text,
|
||||
target.promptText(),
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
|
|
@ -132,19 +132,19 @@ class _AnalyticsActivityView extends StatelessWidget {
|
|||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
// Expanded(
|
||||
// flex: 2,
|
||||
// child: Center(
|
||||
// child: ValueListenableBuilder(
|
||||
// valueListenable: controller.activityConstructId,
|
||||
// builder: (context, constructId, __) => constructId != null
|
||||
// ? _ExampleMessageWidget(
|
||||
// controller.getExampleMessage(constructId),
|
||||
// )
|
||||
// : const SizedBox(),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Center(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller.activityTarget,
|
||||
builder: (context, target, __) => target != null
|
||||
? _ExampleMessageWidget(
|
||||
controller.getExampleMessage(target),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 6,
|
||||
child: _ActivityChoicesWidget(controller),
|
||||
|
|
@ -211,14 +211,15 @@ class _ActivityChoicesWidget extends StatelessWidget {
|
|||
valueListenable: controller.activityState,
|
||||
builder: (context, state, __) {
|
||||
return switch (state) {
|
||||
AsyncLoading<PracticeActivityModel>() => const Center(
|
||||
AsyncLoading<MultipleChoicePracticeActivityModel>() => const Center(
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
),
|
||||
AsyncError<PracticeActivityModel>(:final error) => Column(
|
||||
AsyncError<MultipleChoicePracticeActivityModel>(:final error) =>
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
//allow try to reload activity in case of error
|
||||
|
|
@ -231,11 +232,12 @@ class _ActivityChoicesWidget extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
AsyncLoaded<PracticeActivityModel>(:final value) => LayoutBuilder(
|
||||
AsyncLoaded<MultipleChoicePracticeActivityModel>(:final value) =>
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final choices = controller.filteredChoices(
|
||||
value.practiceTarget,
|
||||
value.multipleChoiceContent!,
|
||||
value.multipleChoiceContent,
|
||||
);
|
||||
final constrainedHeight =
|
||||
constraints.maxHeight.clamp(0.0, 400.0);
|
||||
|
|
@ -281,7 +283,7 @@ class _ActivityChoicesWidget extends StatelessWidget {
|
|||
}
|
||||
|
||||
class _ChoiceCard extends StatelessWidget {
|
||||
final PracticeActivityModel activity;
|
||||
final MultipleChoicePracticeActivityModel activity;
|
||||
final String choiceId;
|
||||
final String targetId;
|
||||
final VoidCallback onPressed;
|
||||
|
|
@ -302,7 +304,7 @@ class _ChoiceCard extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isCorrect = activity.multipleChoiceContent!.isCorrect(choiceId);
|
||||
final isCorrect = activity.multipleChoiceContent.isCorrect(choiceId);
|
||||
final activityType = activity.activityType;
|
||||
final constructId = activity.targetTokens.first.vocabConstructID;
|
||||
|
||||
|
|
|
|||
|
|
@ -50,8 +50,7 @@ class MorphCategoryActivityGenerator {
|
|||
choices.add(morphTag);
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activityType: req.targetType,
|
||||
activity: MorphCategoryPracticeActivityModel(
|
||||
targetTokens: [req.targetTokens.first],
|
||||
langCode: req.userL2,
|
||||
morphFeature: feature,
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ class VocabAudioActivityGenerator {
|
|||
choicesList.shuffle();
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activityType: req.targetType,
|
||||
activity: VocabAudioPracticeActivityModel(
|
||||
targetTokens: [token],
|
||||
langCode: req.userL2,
|
||||
multipleChoiceContent: MultipleChoiceActivity(
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ class VocabMeaningActivityGenerator {
|
|||
final Set<String> constructIdChoices = choices.map((c) => c.string).toSet();
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activityType: req.targetType,
|
||||
activity: VocabMeaningPracticeActivityModel(
|
||||
targetTokens: [token],
|
||||
langCode: req.userL2,
|
||||
multipleChoiceContent: MultipleChoiceActivity(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'package:async/async.dart';
|
|||
import 'package:fluffychat/pangea/constructs/construct_form.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.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/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_match.dart';
|
||||
|
|
@ -65,8 +64,7 @@ class EmojiActivityGenerator {
|
|||
}
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activityType: ActivityTypeEnum.emoji,
|
||||
activity: EmojiPracticeActivityModel(
|
||||
targetTokens: req.targetTokens,
|
||||
langCode: req.userL2,
|
||||
matchContent: PracticeMatchActivity(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.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';
|
||||
|
|
@ -22,8 +21,7 @@ class LemmaActivityGenerator {
|
|||
|
||||
// TODO - modify MultipleChoiceActivity flow to allow no correct answer
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activityType: ActivityTypeEnum.lemmaId,
|
||||
activity: LemmaPracticeActivityModel(
|
||||
targetTokens: [token],
|
||||
langCode: req.userL2,
|
||||
multipleChoiceContent: MultipleChoiceActivity(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:async/async.dart';
|
|||
|
||||
import 'package:fluffychat/pangea/constructs/construct_form.dart';
|
||||
import 'package:fluffychat/pangea/lemmas/lemma_info_response.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/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_match.dart';
|
||||
|
|
@ -33,8 +32,7 @@ class LemmaMeaningActivityGenerator {
|
|||
);
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activityType: ActivityTypeEnum.wordMeaning,
|
||||
activity: LemmaMeaningPracticeActivityModel(
|
||||
targetTokens: req.targetTokens,
|
||||
langCode: req.userL2,
|
||||
matchContent: PracticeMatchActivity(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ 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';
|
||||
|
||||
// includes feedback text and the bad activity model
|
||||
class ActivityQualityFeedback {
|
||||
|
|
@ -16,15 +17,6 @@ class ActivityQualityFeedback {
|
|||
required this.badActivity,
|
||||
});
|
||||
|
||||
factory ActivityQualityFeedback.fromJson(Map<String, dynamic> json) {
|
||||
return ActivityQualityFeedback(
|
||||
feedbackText: json['feedback_text'] as String,
|
||||
badActivity: PracticeActivityModel.fromJson(
|
||||
json['bad_activity'] as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'feedback_text': feedbackText,
|
||||
|
|
@ -90,6 +82,12 @@ class MessageActivityRequest {
|
|||
};
|
||||
}
|
||||
|
||||
PracticeTarget get practiceTarget => PracticeTarget(
|
||||
activityType: targetType,
|
||||
tokens: targetTokens,
|
||||
morphFeature: targetMorphFeature,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:flutter/foundation.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/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';
|
||||
|
|
@ -38,11 +37,10 @@ class MorphActivityGenerator {
|
|||
debugger(when: kDebugMode && distractors.length < 3);
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activity: MorphMatchPracticeActivityModel(
|
||||
targetTokens: req.targetTokens,
|
||||
langCode: req.userL2,
|
||||
activityType: ActivityTypeEnum.morphId,
|
||||
morphFeature: req.targetMorphFeature,
|
||||
morphFeature: morphFeature,
|
||||
multipleChoiceContent: MultipleChoiceActivity(
|
||||
choices: distractors,
|
||||
answers: {morphTag},
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.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';
|
||||
|
|
@ -16,177 +12,23 @@ 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';
|
||||
|
||||
class PracticeActivityModel {
|
||||
sealed class PracticeActivityModel {
|
||||
final List<PangeaToken> targetTokens;
|
||||
final ActivityTypeEnum activityType;
|
||||
final MorphFeaturesEnum? morphFeature;
|
||||
|
||||
final String langCode;
|
||||
|
||||
final MultipleChoiceActivity? multipleChoiceContent;
|
||||
final PracticeMatchActivity? matchContent;
|
||||
|
||||
PracticeActivityModel({
|
||||
const PracticeActivityModel({
|
||||
required this.targetTokens,
|
||||
required this.langCode,
|
||||
required this.activityType,
|
||||
this.morphFeature,
|
||||
this.multipleChoiceContent,
|
||||
this.matchContent,
|
||||
}) {
|
||||
if (matchContent == null && multipleChoiceContent == null) {
|
||||
debugger(when: kDebugMode);
|
||||
throw ("both matchContent and multipleChoiceContent are null in PracticeActivityModel");
|
||||
}
|
||||
if (matchContent != null && multipleChoiceContent != null) {
|
||||
debugger(when: kDebugMode);
|
||||
throw ("both matchContent and multipleChoiceContent are not null in PracticeActivityModel");
|
||||
}
|
||||
if (activityType == ActivityTypeEnum.morphId && morphFeature == null) {
|
||||
debugger(when: kDebugMode);
|
||||
throw ("morphFeature is null in PracticeActivityModel");
|
||||
}
|
||||
}
|
||||
|
||||
String get useCategory {
|
||||
switch (activityType.constructType) {
|
||||
case ConstructTypeEnum.morph:
|
||||
assert(
|
||||
morphFeature != null,
|
||||
"morphFeature is null in PracticeActivityModel.useCategory",
|
||||
);
|
||||
return morphFeature!.name;
|
||||
case ConstructTypeEnum.vocab:
|
||||
return targetTokens.first.pos;
|
||||
}
|
||||
}
|
||||
|
||||
String get useLemma {
|
||||
switch (activityType.constructType) {
|
||||
case ConstructTypeEnum.morph:
|
||||
assert(
|
||||
morphFeature != null,
|
||||
"morphFeature is null in PracticeActivityModel.useCategory",
|
||||
);
|
||||
final tag = targetTokens.first.getMorphTag(morphFeature!);
|
||||
if (tag == null) {
|
||||
throw ("tag is null in PracticeActivityModel.useLemma");
|
||||
}
|
||||
return tag;
|
||||
case ConstructTypeEnum.vocab:
|
||||
return targetTokens.first.lemma.text;
|
||||
}
|
||||
}
|
||||
|
||||
String get useForm {
|
||||
switch (activityType.constructType) {
|
||||
case ConstructTypeEnum.morph:
|
||||
return targetTokens.first.lemma.form;
|
||||
case ConstructTypeEnum.vocab:
|
||||
return targetTokens.first.lemma.text;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
PracticeTarget get practiceTarget => PracticeTarget(
|
||||
tokens: targetTokens,
|
||||
activityType: activityType,
|
||||
morphFeature: morphFeature,
|
||||
);
|
||||
|
||||
bool onMultipleChoiceSelect(
|
||||
ConstructIdentifier choiceConstruct,
|
||||
String choice,
|
||||
) {
|
||||
if (multipleChoiceContent == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "in onMultipleChoiceSelect with null multipleChoiceContent",
|
||||
s: StackTrace.current,
|
||||
data: toJson(),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// NOTE: the response is associated with the contructId of the choice, not the selected token
|
||||
// example: the user selects the word "cat" to match with the emoji 🐶
|
||||
// the response is associated with correct word "dog", not the word "cat"
|
||||
practiceTarget.record.addResponse(
|
||||
cId: choiceConstruct,
|
||||
target: practiceTarget,
|
||||
text: choice,
|
||||
score: isCorrect ? 1 : 0,
|
||||
);
|
||||
|
||||
return isCorrect;
|
||||
}
|
||||
|
||||
bool onMatch(
|
||||
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;
|
||||
}
|
||||
|
||||
bool isCorrect = false;
|
||||
if (multipleChoiceContent != null) {
|
||||
isCorrect = multipleChoiceContent!.answers.any(
|
||||
(answer) => answer.toLowerCase() == choice.choiceContent.toLowerCase(),
|
||||
);
|
||||
} else {
|
||||
// we check to see if it's in the list of acceptable answers
|
||||
// rather than if the vocabForm is the same because an emoji
|
||||
// could be in multiple constructs so there could be multiple answers
|
||||
final answers = matchContent!.matchInfo[token.vocabForm];
|
||||
debugger(when: answers == null && kDebugMode);
|
||||
isCorrect = answers!.contains(choice.choiceContent);
|
||||
}
|
||||
|
||||
// NOTE: the response is associated with the contructId of the selected token, not the choice
|
||||
// example: the user selects the word "cat" to match with the emoji 🐶
|
||||
// the response is associated with incorrect word "cat", not the word "dog"
|
||||
practiceTarget.record.addResponse(
|
||||
cId: token.vocabConstructID,
|
||||
target: practiceTarget,
|
||||
text: choice.choiceContent,
|
||||
score: isCorrect ? 1 : 0,
|
||||
);
|
||||
|
||||
return isCorrect;
|
||||
}
|
||||
|
||||
factory PracticeActivityModel.fromJson(Map<String, dynamic> json) {
|
||||
// moving from multiple_choice to content as the key
|
||||
// this is to make the model more generic
|
||||
// here for backward compatibility
|
||||
final Map<String, dynamic>? contentMap =
|
||||
(json['content'] ?? json["multiple_choice"]) as Map<String, dynamic>?;
|
||||
|
||||
if (contentMap == null) {
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(data: {"json": json}),
|
||||
);
|
||||
throw ("content is null in PracticeActivityModel.fromJson");
|
||||
}
|
||||
|
||||
if (json['lang_code'] is! String) {
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(data: {"json": json}),
|
||||
|
|
@ -203,58 +45,370 @@ class PracticeActivityModel {
|
|||
throw ("tgt_constructs is not a list in PracticeActivityModel.fromJson");
|
||||
}
|
||||
|
||||
return PracticeActivityModel(
|
||||
langCode: json['lang_code'] as String,
|
||||
activityType: ActivityTypeEnum.fromString(json['activity_type']),
|
||||
multipleChoiceContent: json['content'] != null
|
||||
? MultipleChoiceActivity.fromJson(contentMap)
|
||||
: null,
|
||||
targetTokens: (json['target_tokens'] as List)
|
||||
.map((e) => PangeaToken.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
matchContent: json['match_content'] != null
|
||||
? PracticeMatchActivity.fromJson(contentMap)
|
||||
: null,
|
||||
morphFeature: json['morph_feature'] != null
|
||||
? MorphFeaturesEnumExtension.fromString(
|
||||
json['morph_feature'] as String,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
final type = ActivityTypeEnum.fromString(json['activity_type']);
|
||||
|
||||
final morph = json['morph_feature'] != null
|
||||
? MorphFeaturesEnumExtension.fromString(
|
||||
json['morph_feature'] as String,
|
||||
)
|
||||
: null;
|
||||
|
||||
final tokens = (json['target_tokens'] as List)
|
||||
.map((e) => PangeaToken.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final langCode = json['lang_code'] as String;
|
||||
|
||||
final multipleChoiceContent = json['content'] != null
|
||||
? MultipleChoiceActivity.fromJson(
|
||||
json['content'] as Map<String, dynamic>,
|
||||
)
|
||||
: null;
|
||||
|
||||
final matchContent = json['match_content'] != null
|
||||
? PracticeMatchActivity.fromJson(
|
||||
json['match_content'] as Map<String, dynamic>,
|
||||
)
|
||||
: null;
|
||||
|
||||
switch (type) {
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
assert(
|
||||
morph != null,
|
||||
"morphFeature is null in PracticeActivityModel.fromJson for grammarCategory",
|
||||
);
|
||||
assert(
|
||||
multipleChoiceContent != null,
|
||||
"multipleChoiceContent is null in PracticeActivityModel.fromJson for grammarCategory",
|
||||
);
|
||||
return MorphCategoryPracticeActivityModel(
|
||||
langCode: langCode,
|
||||
targetTokens: tokens,
|
||||
morphFeature: morph!,
|
||||
multipleChoiceContent: multipleChoiceContent!,
|
||||
);
|
||||
case ActivityTypeEnum.lemmaAudio:
|
||||
assert(
|
||||
multipleChoiceContent != null,
|
||||
"multipleChoiceContent is null in PracticeActivityModel.fromJson for lemmaAudio",
|
||||
);
|
||||
return VocabAudioPracticeActivityModel(
|
||||
langCode: langCode,
|
||||
targetTokens: tokens,
|
||||
multipleChoiceContent: multipleChoiceContent!,
|
||||
);
|
||||
case ActivityTypeEnum.lemmaMeaning:
|
||||
assert(
|
||||
multipleChoiceContent != null,
|
||||
"multipleChoiceContent is null in PracticeActivityModel.fromJson for lemmaMeaning",
|
||||
);
|
||||
return VocabMeaningPracticeActivityModel(
|
||||
langCode: langCode,
|
||||
targetTokens: tokens,
|
||||
multipleChoiceContent: multipleChoiceContent!,
|
||||
);
|
||||
case ActivityTypeEnum.emoji:
|
||||
assert(
|
||||
matchContent != null,
|
||||
"matchContent is null in PracticeActivityModel.fromJson for emoji",
|
||||
);
|
||||
return EmojiPracticeActivityModel(
|
||||
langCode: langCode,
|
||||
targetTokens: tokens,
|
||||
matchContent: matchContent!,
|
||||
);
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
assert(
|
||||
multipleChoiceContent != null,
|
||||
"multipleChoiceContent is null in PracticeActivityModel.fromJson for lemmaId",
|
||||
);
|
||||
return LemmaPracticeActivityModel(
|
||||
langCode: langCode,
|
||||
targetTokens: tokens,
|
||||
multipleChoiceContent: multipleChoiceContent!,
|
||||
);
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
assert(
|
||||
matchContent != null,
|
||||
"matchContent is null in PracticeActivityModel.fromJson for wordMeaning",
|
||||
);
|
||||
return LemmaMeaningPracticeActivityModel(
|
||||
langCode: langCode,
|
||||
targetTokens: tokens,
|
||||
matchContent: matchContent!,
|
||||
);
|
||||
case ActivityTypeEnum.morphId:
|
||||
assert(
|
||||
morph != null,
|
||||
"morphFeature is null in PracticeActivityModel.fromJson for morphId",
|
||||
);
|
||||
assert(
|
||||
multipleChoiceContent != null,
|
||||
"multipleChoiceContent is null in PracticeActivityModel.fromJson for morphId",
|
||||
);
|
||||
return MorphMatchPracticeActivityModel(
|
||||
langCode: langCode,
|
||||
targetTokens: tokens,
|
||||
morphFeature: morph!,
|
||||
multipleChoiceContent: multipleChoiceContent!,
|
||||
);
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
assert(
|
||||
matchContent != null,
|
||||
"matchContent is null in PracticeActivityModel.fromJson for wordFocusListening",
|
||||
);
|
||||
return WordListeningPracticeActivityModel(
|
||||
langCode: langCode,
|
||||
targetTokens: tokens,
|
||||
matchContent: matchContent!,
|
||||
);
|
||||
default:
|
||||
throw ("Unsupported activity type in PracticeActivityModel.fromJson: $type");
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'lang_code': langCode,
|
||||
'activity_type': activityType.name,
|
||||
'content': multipleChoiceContent?.toJson(),
|
||||
'target_tokens': targetTokens.map((e) => e.toJson()).toList(),
|
||||
'match_content': matchContent?.toJson(),
|
||||
'morph_feature': morphFeature?.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// override operator == and hashCode
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
sealed class MultipleChoicePracticeActivityModel extends PracticeActivityModel {
|
||||
final MultipleChoiceActivity multipleChoiceContent;
|
||||
|
||||
return other is PracticeActivityModel &&
|
||||
const ListEquality().equals(other.targetTokens, targetTokens) &&
|
||||
other.langCode == langCode &&
|
||||
other.activityType == activityType &&
|
||||
other.multipleChoiceContent == multipleChoiceContent &&
|
||||
other.matchContent == matchContent &&
|
||||
other.morphFeature == morphFeature;
|
||||
MultipleChoicePracticeActivityModel({
|
||||
required super.targetTokens,
|
||||
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;
|
||||
}
|
||||
|
||||
OneConstructUse constructUse(String choiceContent) {
|
||||
final correct = multipleChoiceContent.isCorrect(choiceContent);
|
||||
final useType =
|
||||
correct ? activityType.correctUse : activityType.incorrectUse;
|
||||
|
||||
return OneConstructUse(
|
||||
useType: useType,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
metadata: ConstructUseMetaData(
|
||||
roomId: null,
|
||||
timeStamp: DateTime.now(),
|
||||
),
|
||||
category: targetTokens.first.pos,
|
||||
lemma: targetTokens.first.lemma.text,
|
||||
form: targetTokens.first.lemma.text,
|
||||
xp: useType.pointValue,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return const ListEquality().hash(targetTokens) ^
|
||||
langCode.hashCode ^
|
||||
activityType.hashCode ^
|
||||
multipleChoiceContent.hashCode ^
|
||||
matchContent.hashCode ^
|
||||
morphFeature.hashCode;
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = super.toJson();
|
||||
json['content'] = multipleChoiceContent.toJson();
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class MatchPracticeActivityModel extends PracticeActivityModel {
|
||||
final PracticeMatchActivity matchContent;
|
||||
|
||||
MatchPracticeActivityModel({
|
||||
required super.targetTokens,
|
||||
required super.langCode,
|
||||
required super.activityType,
|
||||
required this.matchContent,
|
||||
});
|
||||
|
||||
bool onMatch(
|
||||
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;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = super.toJson();
|
||||
json['match_content'] = matchContent.toJson();
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class MorphPracticeActivityModel
|
||||
extends MultipleChoicePracticeActivityModel {
|
||||
final MorphFeaturesEnum morphFeature;
|
||||
|
||||
MorphPracticeActivityModel({
|
||||
required super.targetTokens,
|
||||
required super.langCode,
|
||||
required super.activityType,
|
||||
required super.multipleChoiceContent,
|
||||
required this.morphFeature,
|
||||
});
|
||||
|
||||
@override
|
||||
PracticeTarget get practiceTarget => PracticeTarget(
|
||||
tokens: targetTokens,
|
||||
activityType: activityType,
|
||||
morphFeature: morphFeature,
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = super.toJson();
|
||||
json['morph_feature'] = morphFeature.name;
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
class MorphCategoryPracticeActivityModel extends MorphPracticeActivityModel {
|
||||
MorphCategoryPracticeActivityModel({
|
||||
required super.targetTokens,
|
||||
required super.langCode,
|
||||
required super.morphFeature,
|
||||
required super.multipleChoiceContent,
|
||||
}) : super(
|
||||
activityType: ActivityTypeEnum.grammarCategory,
|
||||
);
|
||||
|
||||
@override
|
||||
OneConstructUse constructUse(String choiceContent) {
|
||||
final correct = multipleChoiceContent.isCorrect(choiceContent);
|
||||
final useType =
|
||||
correct ? activityType.correctUse : activityType.incorrectUse;
|
||||
final tag = targetTokens.first.getMorphTag(morphFeature)!;
|
||||
|
||||
return OneConstructUse(
|
||||
useType: useType,
|
||||
constructType: ConstructTypeEnum.morph,
|
||||
metadata: ConstructUseMetaData(
|
||||
roomId: null,
|
||||
timeStamp: DateTime.now(),
|
||||
),
|
||||
category: morphFeature.name,
|
||||
lemma: tag,
|
||||
form: targetTokens.first.lemma.form,
|
||||
xp: useType.pointValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MorphMatchPracticeActivityModel extends MorphPracticeActivityModel {
|
||||
MorphMatchPracticeActivityModel({
|
||||
required super.targetTokens,
|
||||
required super.langCode,
|
||||
required super.morphFeature,
|
||||
required super.multipleChoiceContent,
|
||||
}) : super(
|
||||
activityType: ActivityTypeEnum.morphId,
|
||||
);
|
||||
}
|
||||
|
||||
class VocabAudioPracticeActivityModel
|
||||
extends MultipleChoicePracticeActivityModel {
|
||||
VocabAudioPracticeActivityModel({
|
||||
required super.targetTokens,
|
||||
required super.langCode,
|
||||
required super.multipleChoiceContent,
|
||||
}) : super(
|
||||
activityType: ActivityTypeEnum.lemmaAudio,
|
||||
);
|
||||
}
|
||||
|
||||
class VocabMeaningPracticeActivityModel
|
||||
extends MultipleChoicePracticeActivityModel {
|
||||
VocabMeaningPracticeActivityModel({
|
||||
required super.targetTokens,
|
||||
required super.langCode,
|
||||
required super.multipleChoiceContent,
|
||||
}) : super(
|
||||
activityType: ActivityTypeEnum.lemmaMeaning,
|
||||
);
|
||||
}
|
||||
|
||||
class LemmaPracticeActivityModel extends MultipleChoicePracticeActivityModel {
|
||||
LemmaPracticeActivityModel({
|
||||
required super.targetTokens,
|
||||
required super.langCode,
|
||||
required super.multipleChoiceContent,
|
||||
}) : super(
|
||||
activityType: ActivityTypeEnum.lemmaId,
|
||||
);
|
||||
}
|
||||
|
||||
class EmojiPracticeActivityModel extends MatchPracticeActivityModel {
|
||||
EmojiPracticeActivityModel({
|
||||
required super.targetTokens,
|
||||
required super.langCode,
|
||||
required super.matchContent,
|
||||
}) : super(
|
||||
activityType: ActivityTypeEnum.emoji,
|
||||
);
|
||||
}
|
||||
|
||||
class LemmaMeaningPracticeActivityModel extends MatchPracticeActivityModel {
|
||||
LemmaMeaningPracticeActivityModel({
|
||||
required super.targetTokens,
|
||||
required super.langCode,
|
||||
required super.matchContent,
|
||||
}) : super(
|
||||
activityType: ActivityTypeEnum.wordMeaning,
|
||||
);
|
||||
}
|
||||
|
||||
class WordListeningPracticeActivityModel extends MatchPracticeActivityModel {
|
||||
WordListeningPracticeActivityModel({
|
||||
required super.targetTokens,
|
||||
required super.langCode,
|
||||
required super.matchContent,
|
||||
}) : super(
|
||||
activityType: ActivityTypeEnum.wordFocusListening,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,4 +164,13 @@ class PracticeTarget {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String promptText() {
|
||||
switch (activityType) {
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return "${tokens.first.vocabConstructID.lemma}: ${morphFeature!.name}";
|
||||
default:
|
||||
return tokens.first.vocabConstructID.lemma;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:fluffychat/pangea/constructs/construct_form.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/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_match.dart';
|
||||
|
|
@ -15,8 +14,7 @@ class WordFocusListeningGenerator {
|
|||
}
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activityType: ActivityTypeEnum.wordFocusListening,
|
||||
activity: WordListeningPracticeActivityModel(
|
||||
targetTokens: req.targetTokens,
|
||||
langCode: req.userL2,
|
||||
matchContent: PracticeMatchActivity(
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const int numberOfMorphDistractors = 3;
|
|||
|
||||
class MessageMorphInputBarContent extends StatefulWidget {
|
||||
final PracticeController controller;
|
||||
final PracticeActivityModel activity;
|
||||
final MorphPracticeActivityModel activity;
|
||||
final PangeaToken? selectedToken;
|
||||
final double maxWidth;
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ class MessageMorphInputBarContentState
|
|||
String? selectedTag;
|
||||
|
||||
PangeaToken get token => widget.activity.targetTokens.first;
|
||||
MorphFeaturesEnum get morph => widget.activity.morphFeature!;
|
||||
MorphFeaturesEnum get morph => widget.activity.morphFeature;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MessageMorphInputBarContent oldWidget) {
|
||||
|
|
@ -114,7 +114,7 @@ class MessageMorphInputBarContentState
|
|||
runAlignment: WrapAlignment.center,
|
||||
spacing: spacing,
|
||||
runSpacing: spacing,
|
||||
children: widget.activity.multipleChoiceContent!.choices.mapIndexed(
|
||||
children: widget.activity.multipleChoiceContent.choices.mapIndexed(
|
||||
(index, choice) {
|
||||
final wasCorrect =
|
||||
widget.activity.practiceTarget.wasCorrectChoice(choice);
|
||||
|
|
@ -137,7 +137,7 @@ class MessageMorphInputBarContentState
|
|||
form: ConstructForm(
|
||||
cId: widget.activity.targetTokens.first
|
||||
.morphIdByFeature(
|
||||
widget.activity.morphFeature!,
|
||||
widget.activity.morphFeature,
|
||||
)!,
|
||||
form: token.text.content,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -98,17 +98,20 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
AsyncError() => CardErrorWidget(
|
||||
L10n.of(context).errorFetchingActivity,
|
||||
),
|
||||
AsyncLoaded() => state.value.multipleChoiceContent != null
|
||||
? MessageMorphInputBarContent(
|
||||
AsyncLoaded() => switch (state.value) {
|
||||
MultipleChoicePracticeActivityModel() =>
|
||||
MessageMorphInputBarContent(
|
||||
controller: widget.controller,
|
||||
activity: state.value,
|
||||
activity: state.value as MorphPracticeActivityModel,
|
||||
selectedToken: widget.selectedToken,
|
||||
maxWidth: widget.maxWidth,
|
||||
)
|
||||
: MatchActivityCard(
|
||||
currentActivity: state.value,
|
||||
),
|
||||
MatchPracticeActivityModel() => MatchActivityCard(
|
||||
currentActivity:
|
||||
state.value as MatchPracticeActivityModel,
|
||||
controller: widget.controller,
|
||||
),
|
||||
},
|
||||
_ => const SizedBox.shrink(),
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -35,8 +35,6 @@ class PracticeController with ChangeNotifier {
|
|||
MorphSelection? selectedMorph;
|
||||
PracticeChoice? selectedChoice;
|
||||
|
||||
PracticeActivityModel? get activity => _activity;
|
||||
|
||||
PracticeSelection? practiceSelection;
|
||||
|
||||
bool get isTotallyDone =>
|
||||
|
|
@ -65,12 +63,11 @@ class PracticeController with ChangeNotifier {
|
|||
return target == null;
|
||||
}
|
||||
|
||||
return target == null ||
|
||||
target.isCompleteByToken(
|
||||
token,
|
||||
_activity?.morphFeature,
|
||||
) ==
|
||||
true;
|
||||
final morph = _activity is MorphPracticeActivityModel
|
||||
? (_activity as MorphPracticeActivityModel).morphFeature
|
||||
: null;
|
||||
|
||||
return target == null || target.isCompleteByToken(token, morph) == true;
|
||||
}
|
||||
|
||||
bool get showChoiceShimmer {
|
||||
|
|
@ -151,11 +148,13 @@ class PracticeController with ChangeNotifier {
|
|||
|
||||
void onMatch(PangeaToken token, PracticeChoice choice) {
|
||||
if (_activity == null) return;
|
||||
|
||||
final isCorrect = _activity!.activityType == ActivityTypeEnum.morphId
|
||||
? _activity!
|
||||
.onMultipleChoiceSelect(choice.form.cId, choice.choiceContent)
|
||||
: _activity!.onMatch(token, choice);
|
||||
final isCorrect = switch (_activity!) {
|
||||
MultipleChoicePracticeActivityModel() =>
|
||||
(_activity as MultipleChoicePracticeActivityModel)
|
||||
.onMultipleChoiceSelect(choice.form.cId, choice.choiceContent),
|
||||
MatchPracticeActivityModel() =>
|
||||
(_activity as MatchPracticeActivityModel).onMatch(token, choice),
|
||||
};
|
||||
|
||||
final targetId =
|
||||
"message-token-${token.text.uniqueKey}-${pangeaMessageEvent.eventId}";
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import 'package:fluffychat/pangea/toolbar/message_practice/practice_controller.d
|
|||
import 'package:fluffychat/pangea/toolbar/message_practice/practice_match_item.dart';
|
||||
|
||||
class MatchActivityCard extends StatelessWidget {
|
||||
final PracticeActivityModel currentActivity;
|
||||
final MatchPracticeActivityModel currentActivity;
|
||||
final PracticeController controller;
|
||||
|
||||
const MatchActivityCard({
|
||||
|
|
@ -25,8 +25,6 @@ class MatchActivityCard extends StatelessWidget {
|
|||
required this.controller,
|
||||
});
|
||||
|
||||
PracticeActivityModel get activity => currentActivity;
|
||||
|
||||
ActivityTypeEnum get activityType => currentActivity.activityType;
|
||||
|
||||
Widget choiceDisplayContent(
|
||||
|
|
@ -83,7 +81,7 @@ class MatchActivityCard extends StatelessWidget {
|
|||
alignment: WrapAlignment.center,
|
||||
spacing: 4.0,
|
||||
runSpacing: 4.0,
|
||||
children: activity.matchContent!.choices.map(
|
||||
children: currentActivity.matchContent.choices.map(
|
||||
(PracticeChoice cf) {
|
||||
final bool? wasCorrect =
|
||||
currentActivity.practiceTarget.wasCorrectMatch(cf);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue