diff --git a/lib/pages/chat/events/html_message.dart b/lib/pages/chat/events/html_message.dart
index 0f54e6285..739bef2ea 100644
--- a/lib/pages/chat/events/html_message.dart
+++ b/lib/pages/chat/events/html_message.dart
@@ -438,6 +438,8 @@ class HtmlMessage extends StatelessWidget {
pangeaMessageEvent != null && !pangeaMessageEvent!.ownMessage
? TokensUtil.getNewTokensByEvent(pangeaMessageEvent!)
: [];
+
+ final practiceMode = overlayController?.practiceController.practiceMode;
// Pangea#
switch (node.localName) {
@@ -559,10 +561,7 @@ class HtmlMessage extends StatelessWidget {
curve: Curves.easeOut,
child: SizedBox(
height:
- overlayController!
- .practiceController
- .practiceMode !=
- MessagePracticeMode.noneSelected
+ practiceMode != MessagePracticeMode.noneSelected
? 4.0
: 0.0,
width: tokenWidth,
@@ -1003,9 +1002,7 @@ class HtmlMessage extends StatelessWidget {
),
curve: Curves.easeOut,
child: SizedBox(
- height:
- overlayController!.practiceController.practiceMode !=
- MessagePracticeMode.noneSelected
+ height: practiceMode != MessagePracticeMode.noneSelected
? 4.0
: 0.0,
width: 0,
diff --git a/lib/pangea/analytics_misc/construct_practice_extension.dart b/lib/pangea/analytics_misc/construct_practice_extension.dart
new file mode 100644
index 000000000..ebbf91b71
--- /dev/null
+++ b/lib/pangea/analytics_misc/construct_practice_extension.dart
@@ -0,0 +1,14 @@
+import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
+import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
+
+extension ConstructPracticeExtension on List {
+ List practiceSort(ActivityTypeEnum type) {
+ final sorted = List.from(this);
+ sorted.sort((a, b) {
+ final scoreA = a.practiceScore(activityType: type);
+ final scoreB = b.practiceScore(activityType: type);
+ return scoreB.compareTo(scoreA);
+ });
+ return sorted;
+ }
+}
diff --git a/lib/pangea/analytics_misc/example_message_util.dart b/lib/pangea/analytics_misc/example_message_util.dart
index 498c6129a..d8ba12dfd 100644
--- a/lib/pangea/analytics_misc/example_message_util.dart
+++ b/lib/pangea/analytics_misc/example_message_util.dart
@@ -4,9 +4,11 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
+import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
+import 'package:fluffychat/widgets/matrix.dart';
/// Internal result class that holds all computed data from building an example message.
class _ExampleMessageResult {
@@ -37,14 +39,12 @@ class _ExampleMessageResult {
class ExampleMessageUtil {
static Future?> getExampleMessage(
- ConstructUses construct,
- Client client, {
+ ConstructUses construct, {
String? form,
bool noBold = false,
}) async {
final result = await _getExampleMessageResult(
construct,
- client,
form: form,
noBold: noBold,
);
@@ -52,14 +52,12 @@ class ExampleMessageUtil {
}
static Future getAudioExampleMessage(
- ConstructUses construct,
- Client client, {
+ ConstructUses construct, {
String? form,
bool noBold = false,
}) async {
final result = await _getExampleMessageResult(
construct,
- client,
form: form,
noBold: noBold,
);
@@ -87,14 +85,16 @@ class ExampleMessageUtil {
}
static Future<_ExampleMessageResult?> _getExampleMessageResult(
- ConstructUses construct,
- Client client, {
+ ConstructUses construct, {
String? form,
bool noBold = false,
}) async {
- for (final use in construct.cappedUses) {
- if (form != null && use.form != form) continue;
+ final uses = List.from(construct.cappedUses);
+ uses.shuffle(); // Shuffle to get a random example message each time
+ for (final use in uses) {
+ if (form != null && use.form != form) continue;
+ final client = MatrixState.pangeaController.matrixState.client;
final event = await client.getEventByConstructUse(use);
if (event == null) continue;
diff --git a/lib/pangea/analytics_practice/analytics_practice_constants.dart b/lib/pangea/analytics_practice/analytics_practice_constants.dart
index a8e06083c..a6e50ee56 100644
--- a/lib/pangea/analytics_practice/analytics_practice_constants.dart
+++ b/lib/pangea/analytics_practice/analytics_practice_constants.dart
@@ -2,4 +2,5 @@ class AnalyticsPracticeConstants {
static const int timeForBonus = 60;
static const int practiceGroupSize = 10;
static const int errorBufferSize = 5;
+ static int get targetsToGenerate => practiceGroupSize + errorBufferSize;
}
diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart
index ec492a762..66f4f9966 100644
--- a/lib/pangea/analytics_practice/analytics_practice_page.dart
+++ b/lib/pangea/analytics_practice/analytics_practice_page.dart
@@ -636,11 +636,11 @@ class AnalyticsPracticeState extends State
activity,
);
- final use = activity.constructUse(choiceContent);
- _sessionLoader.value!.submitAnswer(use);
+ final uses = activity.constructUses(choiceContent);
+ _sessionLoader.value!.submitAnswer(uses);
await _analyticsService.updateService.addAnalytics(
choiceTargetId(choiceContent),
- [use],
+ uses,
_l2!.langCodeShort,
);
@@ -696,7 +696,6 @@ class AnalyticsPracticeState extends State
return ExampleMessageUtil.getExampleMessage(
await _analyticsService.getConstructUse(construct, _l2!.langCodeShort),
- Matrix.of(context).client,
);
}
diff --git a/lib/pangea/analytics_practice/analytics_practice_session_model.dart b/lib/pangea/analytics_practice/analytics_practice_session_model.dart
index c7c58cfc3..279bdf402 100644
--- a/lib/pangea/analytics_practice/analytics_practice_session_model.dart
+++ b/lib/pangea/analytics_practice/analytics_practice_session_model.dart
@@ -1,4 +1,4 @@
-import 'package:flutter/painting.dart';
+import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
@@ -128,11 +128,9 @@ class AnalyticsPracticeSessionModel {
}) : state = state ?? const AnalyticsPracticeSessionState();
// Maximum activities to attempt (including skips)
- int get _maxAttempts =>
- (AnalyticsPracticeConstants.practiceGroupSize +
- AnalyticsPracticeConstants.errorBufferSize)
- .clamp(0, practiceTargets.length)
- .toInt();
+ int get _maxAttempts => AnalyticsPracticeConstants.targetsToGenerate
+ .clamp(0, practiceTargets.length)
+ .toInt();
int get _completionGoal => AnalyticsPracticeConstants.practiceGroupSize.clamp(
0,
@@ -186,8 +184,8 @@ class AnalyticsPracticeSessionModel {
void incrementSkippedActivities() =>
state = state.copyWith(skippedActivities: state.skippedActivities + 1);
- void submitAnswer(OneConstructUse use) =>
- state = state.copyWith(completedUses: [...state.completedUses, use]);
+ void submitAnswer(List uses) =>
+ state = state.copyWith(completedUses: [...state.completedUses, ...uses]);
factory AnalyticsPracticeSessionModel.fromJson(Map json) {
return AnalyticsPracticeSessionModel(
diff --git a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart
index c0c6f3b1d..6df910d67 100644
--- a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart
+++ b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart
@@ -1,26 +1,13 @@
import 'dart:math';
-import 'package:flutter/material.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/example_message_util.dart';
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constants.dart';
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
+import 'package:fluffychat/pangea/analytics_practice/grammar_error_target_generator.dart';
+import 'package:fluffychat/pangea/analytics_practice/grammar_match_target_generator.dart';
+import 'package:fluffychat/pangea/analytics_practice/vocab_audio_target_generator.dart';
+import 'package:fluffychat/pangea/analytics_practice/vocab_meaning_target_generator.dart';
import 'package:fluffychat/pangea/common/network/requests.dart';
-import 'package:fluffychat/pangea/common/utils/error_handler.dart';
-import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
-import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
-import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
-import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
-import 'package:fluffychat/pangea/languages/language_constants.dart';
-import 'package:fluffychat/pangea/lemmas/lemma.dart';
-import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
-import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart';
-import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_request.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_target.dart';
import 'package:fluffychat/widgets/matrix.dart';
class InsufficientDataException implements Exception {}
@@ -36,73 +23,48 @@ class AnalyticsPracticeSessionRepo {
}
final List targets = [];
+ final analytics =
+ MatrixState.pangeaController.matrixState.analyticsDataService;
+
+ final vocabConstructs = await analytics
+ .getAggregatedConstructs(ConstructTypeEnum.vocab, language)
+ .then((map) => map.values.toList());
if (type == ConstructTypeEnum.vocab) {
- const totalNeeded =
- AnalyticsPracticeConstants.practiceGroupSize +
- AnalyticsPracticeConstants.errorBufferSize;
+ final totalNeeded = AnalyticsPracticeConstants.targetsToGenerate;
final halfNeeded = (totalNeeded / 2).ceil();
// Fetch audio constructs (with example messages)
- final audioMap = await _fetchAudio(language);
- final audioCount = min(audioMap.length, halfNeeded);
+ final audioTargets = await VocabAudioTargetGenerator.get(vocabConstructs);
+ final audioCount = min(audioTargets.length, halfNeeded);
// Fetch vocab constructs to fill the rest
final vocabNeeded = totalNeeded - audioCount;
- final vocabConstructs = await _fetchVocab(language);
- final vocabCount = min(vocabConstructs.length, vocabNeeded);
+ final vocabTargets = await VocabMeaningTargetGenerator.get(
+ vocabConstructs,
+ );
+ final vocabCount = min(vocabTargets.length, vocabNeeded);
- for (final entry in audioMap.entries.take(audioCount)) {
- targets.add(
- AnalyticsActivityTarget(
- target: PracticeTarget(
- tokens: [entry.key.asToken],
- activityType: ActivityTypeEnum.lemmaAudio,
- ),
- audioExampleMessage: entry.value,
- ),
- );
- }
- for (var i = 0; i < vocabCount; i++) {
- targets.add(
- AnalyticsActivityTarget(
- target: PracticeTarget(
- tokens: [vocabConstructs[i].asToken],
- activityType: ActivityTypeEnum.lemmaMeaning,
- ),
- ),
- );
- }
- targets.shuffle();
+ final audioTargetsToAdd = audioTargets.take(audioCount);
+ final meaningTargetsToAdd = vocabTargets.take(vocabCount);
+ targets.addAll(audioTargetsToAdd);
+ targets.addAll(meaningTargetsToAdd);
} else {
- final errorTargets = await _fetchErrors(language);
+ final errorTargets = await GrammarErrorTargetGenerator.get(
+ vocabConstructs,
+ );
targets.addAll(errorTargets);
- if (targets.length <
- (AnalyticsPracticeConstants.practiceGroupSize +
- AnalyticsPracticeConstants.errorBufferSize)) {
- final morphs = await _fetchMorphs(language);
+
+ if (targets.length < AnalyticsPracticeConstants.targetsToGenerate) {
+ final morphConstructs = await analytics
+ .getAggregatedConstructs(ConstructTypeEnum.morph, language)
+ .then((map) => map.values.toList());
+ final morphs = await GrammarMatchTargetGenerator.get(morphConstructs);
final remainingCount =
- (AnalyticsPracticeConstants.practiceGroupSize +
- AnalyticsPracticeConstants.errorBufferSize) -
- targets.length;
+ AnalyticsPracticeConstants.targetsToGenerate - targets.length;
+
final morphEntries = morphs.take(remainingCount);
-
- for (final entry in morphEntries) {
- targets.add(
- AnalyticsActivityTarget(
- target: PracticeTarget(
- tokens: [entry.token],
- activityType: ActivityTypeEnum.grammarCategory,
- morphFeature: entry.feature,
- ),
- exampleMessage: ExampleMessageInfo(
- exampleMessage: entry.exampleMessage,
- ),
- ),
- );
- }
-
- targets.shuffle();
+ targets.addAll(morphEntries);
}
}
@@ -110,6 +72,7 @@ class AnalyticsPracticeSessionRepo {
throw InsufficientDataException();
}
+ targets.shuffle();
final session = AnalyticsPracticeSessionModel(
userL1: MatrixState.pangeaController.userController.userL1!.langCode,
userL2: MatrixState.pangeaController.userController.userL2!.langCode,
@@ -118,391 +81,4 @@ class AnalyticsPracticeSessionRepo {
);
return session;
}
-
- static Future> _fetchVocab(String language) async {
- final constructs = await MatrixState
- .pangeaController
- .matrixState
- .analyticsDataService
- .getAggregatedConstructs(ConstructTypeEnum.vocab, language)
- .then((map) => map.values.toList());
-
- // Score and sort by priority (highest first). Uses shared scorer for
- // consistent prioritization with message practice.
- constructs.sort((a, b) {
- final scoreA = a.practiceScore(
- activityType: ActivityTypeEnum.lemmaMeaning,
- );
- final scoreB = b.practiceScore(
- activityType: ActivityTypeEnum.lemmaMeaning,
- );
- return scoreB.compareTo(scoreA);
- });
-
- final Set seemLemmas = {};
- final targets = [];
- for (final construct in constructs) {
- if (seemLemmas.contains(construct.lemma)) continue;
- seemLemmas.add(construct.lemma);
- targets.add(construct.id);
- if (targets.length >=
- (AnalyticsPracticeConstants.practiceGroupSize +
- AnalyticsPracticeConstants.errorBufferSize)) {
- break;
- }
- }
- return targets;
- }
-
- static Future