Merge pull request #5197 from pangeachat/5072-grammar-practice
5072 grammar practice
This commit is contained in:
commit
af26fd3bd9
44 changed files with 1271 additions and 808 deletions
|
|
@ -38,6 +38,7 @@ import 'package:fluffychat/pangea/analytics_misc/analytics_navigation_util.dart'
|
|||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_page/activity_archive.dart';
|
||||
import 'package:fluffychat/pangea/analytics_page/empty_analytics_page.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_page.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/level_analytics_details_content.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/chat_settings/pages/edit_course.dart';
|
||||
|
|
@ -59,7 +60,6 @@ import 'package:fluffychat/pangea/login/pages/signup.dart';
|
|||
import 'package:fluffychat/pangea/space_analytics/space_analytics.dart';
|
||||
import 'package:fluffychat/pangea/spaces/space_constants.dart';
|
||||
import 'package:fluffychat/pangea/subscription/pages/settings_subscription.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_page.dart';
|
||||
import 'package:fluffychat/widgets/config_viewer.dart';
|
||||
import 'package:fluffychat/widgets/layouts/empty_page.dart';
|
||||
import 'package:fluffychat/widgets/layouts/two_column_layout.dart';
|
||||
|
|
@ -542,6 +542,18 @@ abstract class AppRoutes {
|
|||
),
|
||||
redirect: loggedOutRedirect,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'practice',
|
||||
pageBuilder: (context, state) {
|
||||
return defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const AnalyticsPractice(
|
||||
type: ConstructTypeEnum.morph,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: ':construct',
|
||||
pageBuilder: (context, state) {
|
||||
|
|
@ -580,7 +592,9 @@ abstract class AppRoutes {
|
|||
return defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const VocabPractice(),
|
||||
const AnalyticsPractice(
|
||||
type: ConstructTypeEnum.vocab,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -5046,5 +5046,9 @@
|
|||
"voice": "Voice",
|
||||
"youLeftTheChat": "🚪 You left the chat",
|
||||
"downloadInitiated": "Download initiated",
|
||||
"webDownloadPermissionMessage": "If your browser blocks downloads, please enable downloads for this site."
|
||||
"webDownloadPermissionMessage": "If your browser blocks downloads, please enable downloads for this site.",
|
||||
"practiceGrammar": "Practice grammar",
|
||||
"notEnoughToPractice": "Send more messages to unlock practice",
|
||||
"constructUseCorGCDesc": "Correct grammar category practice",
|
||||
"constructUseIncGCDesc": "Incorrect grammar category practice"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,73 +189,67 @@ class ConstructAnalyticsViewState extends State<ConstructAnalyticsView> {
|
|||
),
|
||||
),
|
||||
floatingActionButton:
|
||||
widget.view == ConstructTypeEnum.vocab && widget.construct == null
|
||||
? _buildVocabPracticeButton(context)
|
||||
: null,
|
||||
widget.construct == null ? _PracticeButton(view: widget.view) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildVocabPracticeButton(BuildContext context) {
|
||||
// Check if analytics is loaded first
|
||||
if (MatrixState
|
||||
.pangeaController.matrixState.analyticsDataService.isInitializing) {
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Loading vocabulary data...',
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
label: Text(L10n.of(context).practiceVocab),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
class _PracticeButton extends StatelessWidget {
|
||||
final ConstructTypeEnum view;
|
||||
const _PracticeButton({required this.view});
|
||||
|
||||
void _showSnackbar(BuildContext context, String message) {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final vocabCount = MatrixState
|
||||
.pangeaController.matrixState.analyticsDataService
|
||||
.numConstructs(ConstructTypeEnum.vocab);
|
||||
final hasEnoughVocab = vocabCount >= 10;
|
||||
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: hasEnoughVocab
|
||||
? () {
|
||||
context.go(
|
||||
"/rooms/analytics/${ConstructTypeEnum.vocab.name}/practice",
|
||||
);
|
||||
}
|
||||
: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
L10n.of(context).mustHave10Words,
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor:
|
||||
hasEnoughVocab ? null : Theme.of(context).colorScheme.surfaceContainer,
|
||||
foregroundColor: hasEnoughVocab
|
||||
? null
|
||||
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
hasEnoughVocab ? Symbols.fitness_center : Icons.lock_outline,
|
||||
size: 18,
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final analyticsService = Matrix.of(context).analyticsDataService;
|
||||
if (analyticsService.isInitializing) {
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () => _showSnackbar(
|
||||
context,
|
||||
L10n.of(context).loadingPleaseWait,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(L10n.of(context).practiceVocab),
|
||||
],
|
||||
),
|
||||
);
|
||||
label: Text(view.practiceButtonText(context)),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
);
|
||||
}
|
||||
|
||||
final count = analyticsService.numConstructs(view);
|
||||
final enabled = count >= 10;
|
||||
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: enabled
|
||||
? () => context.go("/rooms/analytics/${view.name}/practice")
|
||||
: () => _showSnackbar(
|
||||
context,
|
||||
L10n.of(context).notEnoughToPractice,
|
||||
),
|
||||
backgroundColor:
|
||||
enabled ? null : Theme.of(context).colorScheme.surfaceContainer,
|
||||
foregroundColor: enabled
|
||||
? null
|
||||
: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
enabled ? Symbols.fitness_center : Icons.lock_outline,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(view.practiceButtonText(context)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ class MorphAnalyticsListView extends StatelessWidget {
|
|||
childCount: controller.features.length,
|
||||
),
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 75.0)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,20 +4,15 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
import 'package:fluffychat/pangea/morphs/parts_of_speech_enum.dart';
|
||||
|
||||
enum ConstructTypeEnum {
|
||||
/// for vocabulary words
|
||||
vocab,
|
||||
|
||||
/// for morphs, actually called "Grammar" in the UI... :P
|
||||
morph,
|
||||
}
|
||||
morph;
|
||||
|
||||
extension ConstructExtension on ConstructTypeEnum {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ConstructTypeEnum.vocab:
|
||||
|
|
@ -37,25 +32,6 @@ extension ConstructExtension on ConstructTypeEnum {
|
|||
}
|
||||
}
|
||||
|
||||
int get maxXPPerLemma {
|
||||
switch (this) {
|
||||
case ConstructTypeEnum.vocab:
|
||||
return AnalyticsConstants.vocabUseMaxXP;
|
||||
case ConstructTypeEnum.morph:
|
||||
return AnalyticsConstants.morphUseMaxXP;
|
||||
}
|
||||
}
|
||||
|
||||
String? getDisplayCopy(String category, BuildContext context) {
|
||||
switch (this) {
|
||||
case ConstructTypeEnum.morph:
|
||||
return MorphFeaturesEnumExtension.fromString(category)
|
||||
.getDisplayCopy(context);
|
||||
case ConstructTypeEnum.vocab:
|
||||
return getVocabCategoryName(category, context);
|
||||
}
|
||||
}
|
||||
|
||||
ProgressIndicatorEnum get indicator {
|
||||
switch (this) {
|
||||
case ConstructTypeEnum.morph:
|
||||
|
|
@ -64,9 +40,17 @@ extension ConstructExtension on ConstructTypeEnum {
|
|||
return ProgressIndicatorEnum.wordsUsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ConstructTypeUtil {
|
||||
String practiceButtonText(BuildContext context) {
|
||||
final l10n = L10n.of(context);
|
||||
switch (this) {
|
||||
case ConstructTypeEnum.vocab:
|
||||
return l10n.practiceVocab;
|
||||
case ConstructTypeEnum.morph:
|
||||
return l10n.practiceGrammar;
|
||||
}
|
||||
}
|
||||
|
||||
static ConstructTypeEnum fromString(String? string) {
|
||||
switch (string) {
|
||||
case 'v':
|
||||
|
|
|
|||
|
|
@ -82,6 +82,10 @@ enum ConstructUseTypeEnum {
|
|||
// vocab lemma audio activity
|
||||
corLA,
|
||||
incLA,
|
||||
|
||||
// grammar category activity
|
||||
corGC,
|
||||
incGC,
|
||||
}
|
||||
|
||||
extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
||||
|
|
@ -163,6 +167,10 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
return L10n.of(context).constructUseCorLADesc;
|
||||
case ConstructUseTypeEnum.incLA:
|
||||
return L10n.of(context).constructUseIncLADesc;
|
||||
case ConstructUseTypeEnum.corGC:
|
||||
return L10n.of(context).constructUseCorGCDesc;
|
||||
case ConstructUseTypeEnum.incGC:
|
||||
return L10n.of(context).constructUseIncGCDesc;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,6 +211,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.corM:
|
||||
case ConstructUseTypeEnum.incM:
|
||||
case ConstructUseTypeEnum.ignM:
|
||||
case ConstructUseTypeEnum.corGC:
|
||||
case ConstructUseTypeEnum.incGC:
|
||||
return ActivityTypeEnum.morphId.icon;
|
||||
case ConstructUseTypeEnum.em:
|
||||
return ActivityTypeEnum.emoji.icon;
|
||||
|
|
@ -235,6 +245,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.corM:
|
||||
case ConstructUseTypeEnum.corLM:
|
||||
case ConstructUseTypeEnum.corLA:
|
||||
case ConstructUseTypeEnum.corGC:
|
||||
return 5;
|
||||
|
||||
case ConstructUseTypeEnum.pvm:
|
||||
|
|
@ -275,6 +286,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.incM:
|
||||
case ConstructUseTypeEnum.incLM:
|
||||
case ConstructUseTypeEnum.incLA:
|
||||
case ConstructUseTypeEnum.incGC:
|
||||
return -1;
|
||||
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
|
|
@ -326,6 +338,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.corLA:
|
||||
case ConstructUseTypeEnum.incLA:
|
||||
case ConstructUseTypeEnum.bonus:
|
||||
case ConstructUseTypeEnum.corGC:
|
||||
case ConstructUseTypeEnum.incGC:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -369,6 +383,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.click:
|
||||
case ConstructUseTypeEnum.corLM:
|
||||
case ConstructUseTypeEnum.incLM:
|
||||
case ConstructUseTypeEnum.corGC:
|
||||
case ConstructUseTypeEnum.incGC:
|
||||
return LearningSkillsEnum.reading;
|
||||
case ConstructUseTypeEnum.pvm:
|
||||
return LearningSkillsEnum.speaking;
|
||||
|
|
@ -398,6 +414,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.corMM:
|
||||
case ConstructUseTypeEnum.corLM:
|
||||
case ConstructUseTypeEnum.corLA:
|
||||
case ConstructUseTypeEnum.corGC:
|
||||
return SpaceAnalyticsSummaryEnum.numChoicesCorrect;
|
||||
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
|
|
@ -410,6 +427,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.incMM:
|
||||
case ConstructUseTypeEnum.incLM:
|
||||
case ConstructUseTypeEnum.incLA:
|
||||
case ConstructUseTypeEnum.incGC:
|
||||
return SpaceAnalyticsSummaryEnum.numChoicesIncorrect;
|
||||
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class OneConstructUse {
|
|||
debugger(when: kDebugMode && json['constructType'] == null);
|
||||
|
||||
final ConstructTypeEnum constructType = json['constructType'] != null
|
||||
? ConstructTypeUtil.fromString(json['constructType'])
|
||||
? ConstructTypeEnum.fromString(json['constructType'])
|
||||
: ConstructTypeEnum.vocab;
|
||||
|
||||
final useType = ConstructUseTypeUtil.fromString(json['useType']);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_details_popup/vocab_analytics_list_tile.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/analytics_navigation_util.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';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
class VocabPracticeConstants {
|
||||
class AnalyticsPracticeConstants {
|
||||
static const int timeForBonus = 60;
|
||||
static const int practiceGroupSize = 10;
|
||||
}
|
||||
|
|
@ -10,71 +10,79 @@ 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';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_view.dart';
|
||||
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/text_to_speech/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_session_repo.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_view.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';
|
||||
|
||||
class VocabPracticeChoice {
|
||||
class PracticeChoice {
|
||||
final String choiceId;
|
||||
final String choiceText;
|
||||
final String? choiceEmoji;
|
||||
|
||||
const VocabPracticeChoice({
|
||||
const PracticeChoice({
|
||||
required this.choiceId,
|
||||
required this.choiceText,
|
||||
this.choiceEmoji,
|
||||
});
|
||||
}
|
||||
|
||||
class SessionLoader extends AsyncLoader<VocabPracticeSessionModel> {
|
||||
@override
|
||||
Future<VocabPracticeSessionModel> fetch() => VocabPracticeSessionRepo.get();
|
||||
}
|
||||
|
||||
class VocabPractice extends StatefulWidget {
|
||||
const VocabPractice({super.key});
|
||||
class SessionLoader extends AsyncLoader<AnalyticsPracticeSessionModel> {
|
||||
final ConstructTypeEnum type;
|
||||
SessionLoader({required this.type});
|
||||
|
||||
@override
|
||||
VocabPracticeState createState() => VocabPracticeState();
|
||||
Future<AnalyticsPracticeSessionModel> fetch() =>
|
||||
AnalyticsPracticeSessionRepo.get(type);
|
||||
}
|
||||
|
||||
class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
||||
final SessionLoader _sessionLoader = SessionLoader();
|
||||
class AnalyticsPractice extends StatefulWidget {
|
||||
final ConstructTypeEnum type;
|
||||
const AnalyticsPractice({
|
||||
super.key,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
final ValueNotifier<AsyncState<PracticeActivityModel>> activityState =
|
||||
ValueNotifier(const AsyncState.idle());
|
||||
@override
|
||||
AnalyticsPracticeState createState() => AnalyticsPracticeState();
|
||||
}
|
||||
|
||||
final Queue<MapEntry<ConstructIdentifier, Completer<PracticeActivityModel>>>
|
||||
_queue = Queue();
|
||||
class AnalyticsPracticeState extends State<AnalyticsPractice>
|
||||
with AnalyticsUpdater {
|
||||
late final SessionLoader _sessionLoader;
|
||||
|
||||
final ValueNotifier<ConstructIdentifier?> activityConstructId =
|
||||
ValueNotifier<ConstructIdentifier?>(null);
|
||||
final ValueNotifier<AsyncState<MultipleChoicePracticeActivityModel>>
|
||||
activityState = ValueNotifier(const AsyncState.idle());
|
||||
|
||||
final Queue<
|
||||
MapEntry<PracticeTarget,
|
||||
Completer<MultipleChoicePracticeActivityModel>>> _queue = Queue();
|
||||
|
||||
final ValueNotifier<PracticeTarget?> activityTarget =
|
||||
ValueNotifier<PracticeTarget?>(null);
|
||||
|
||||
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;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sessionLoader = SessionLoader(type: widget.type);
|
||||
_startSession();
|
||||
_languageStreamSubscription = MatrixState
|
||||
.pangeaController.userController.languageStream.stream
|
||||
|
|
@ -85,41 +93,43 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
void dispose() {
|
||||
_languageStreamSubscription?.cancel();
|
||||
if (_isComplete) {
|
||||
VocabPracticeSessionRepo.clear();
|
||||
AnalyticsPracticeSessionRepo.clear();
|
||||
} else {
|
||||
_saveSession();
|
||||
}
|
||||
_sessionLoader.dispose();
|
||||
activityState.dispose();
|
||||
activityConstructId.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;
|
||||
|
||||
ValueNotifier<AsyncState<VocabPracticeSessionModel>> get sessionState =>
|
||||
ValueNotifier<AsyncState<AnalyticsPracticeSessionModel>> get sessionState =>
|
||||
_sessionLoader.state;
|
||||
|
||||
AnalyticsDataService get _analyticsService =>
|
||||
Matrix.of(context).analyticsDataService;
|
||||
|
||||
List<VocabPracticeChoice> filteredChoices(
|
||||
PracticeTarget target,
|
||||
MultipleChoiceActivity activity,
|
||||
List<PracticeChoice> filteredChoices(
|
||||
MultipleChoicePracticeActivityModel activity,
|
||||
) {
|
||||
final choices = activity.choices.toList();
|
||||
final answer = activity.answers.first;
|
||||
final filtered = <VocabPracticeChoice>[];
|
||||
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) {
|
||||
|
|
@ -130,10 +140,10 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
(choice) => choice.choiceText == text,
|
||||
);
|
||||
if (index != -1) {
|
||||
filtered[index] = VocabPracticeChoice(
|
||||
filtered[index] = PracticeChoice(
|
||||
choiceId: id,
|
||||
choiceText: text,
|
||||
choiceEmoji: getChoiceEmoji(target, id),
|
||||
choiceEmoji: getChoiceEmoji(activity.storageKey, id),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
|
|
@ -141,10 +151,10 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
|
||||
seenTexts.add(text);
|
||||
filtered.add(
|
||||
VocabPracticeChoice(
|
||||
PracticeChoice(
|
||||
choiceId: id,
|
||||
choiceText: text,
|
||||
choiceEmoji: getChoiceEmoji(target, id),
|
||||
choiceEmoji: getChoiceEmoji(activity.storageKey, id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -152,24 +162,29 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
return filtered;
|
||||
}
|
||||
|
||||
String getChoiceText(PracticeTarget target, String choiceId) {
|
||||
if (_choiceTexts.containsKey(target) &&
|
||||
_choiceTexts[target]!.containsKey(choiceId)) {
|
||||
return _choiceTexts[target]![choiceId]!;
|
||||
String getChoiceText(String key, String choiceId) {
|
||||
if (widget.type == ConstructTypeEnum.morph) {
|
||||
return 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) =>
|
||||
_choiceEmojis[target]?[choiceId];
|
||||
String? getChoiceEmoji(String key, String choiceId) {
|
||||
if (widget.type == ConstructTypeEnum.morph) return null;
|
||||
return _choiceEmojis[key]?[choiceId];
|
||||
}
|
||||
|
||||
String choiceTargetId(String choiceId) =>
|
||||
'vocab-choice-card-${choiceId.replaceAll(' ', '_')}';
|
||||
'${widget.type.name}-choice-card-${choiceId.replaceAll(' ', '_')}';
|
||||
|
||||
void _resetActivityState() {
|
||||
activityState.value = const AsyncState.loading();
|
||||
activityConstructId.value = null;
|
||||
activityTarget.value = null;
|
||||
}
|
||||
|
||||
void _resetSessionState() {
|
||||
|
|
@ -186,9 +201,21 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
}
|
||||
}
|
||||
|
||||
void _playAudio() {
|
||||
if (activityTarget.value == null) return;
|
||||
if (widget.type != ConstructTypeEnum.vocab) return;
|
||||
TtsController.tryToSpeak(
|
||||
activityTarget.value!.tokens.first.vocabConstructID.lemma,
|
||||
langCode: MatrixState.pangeaController.userController.userL2!.langCode,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveSession() async {
|
||||
if (_sessionLoader.isLoaded) {
|
||||
await VocabPracticeSessionRepo.update(_sessionLoader.value!);
|
||||
await AnalyticsPracticeSessionRepo.update(
|
||||
widget.type,
|
||||
_sessionLoader.value!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -226,7 +253,7 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
Future<void> reloadSession() async {
|
||||
_resetActivityState();
|
||||
_resetSessionState();
|
||||
await VocabPracticeSessionRepo.clear();
|
||||
await AnalyticsPracticeSessionRepo.clear();
|
||||
_sessionLoader.reset();
|
||||
await _startSession();
|
||||
}
|
||||
|
|
@ -251,19 +278,17 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
_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();
|
||||
activityConstructId.value = nextActivityCompleter.key;
|
||||
TtsController.tryToSpeak(
|
||||
nextActivityCompleter.key.lemma,
|
||||
langCode:
|
||||
MatrixState.pangeaController.userController.userL2!.langCode,
|
||||
);
|
||||
|
||||
activityTarget.value = nextActivityCompleter.key;
|
||||
_playAudio();
|
||||
|
||||
final activity = await nextActivityCompleter.value.future;
|
||||
activityState.value = AsyncState.loaded(activity);
|
||||
|
|
@ -284,11 +309,9 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
try {
|
||||
activityState.value = const AsyncState.loading();
|
||||
final req = requests.first;
|
||||
activityConstructId.value = req.targetTokens.first.vocabConstructID;
|
||||
TtsController.tryToSpeak(
|
||||
req.targetTokens.first.vocabConstructID.lemma,
|
||||
langCode: MatrixState.pangeaController.userController.userL2!.langCode,
|
||||
);
|
||||
|
||||
activityTarget.value = req.target;
|
||||
_playAudio();
|
||||
|
||||
final res = await _fetchActivity(req);
|
||||
if (!mounted) return;
|
||||
|
|
@ -305,13 +328,9 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
|
||||
Future<void> _fillActivityQueue(List<MessageActivityRequest> requests) async {
|
||||
for (final request in requests) {
|
||||
final completer = Completer<PracticeActivityModel>();
|
||||
_queue.add(
|
||||
MapEntry(
|
||||
request.targetTokens.first.vocabConstructID,
|
||||
completer,
|
||||
),
|
||||
);
|
||||
final completer = Completer<MultipleChoicePracticeActivityModel>();
|
||||
_queue.add(MapEntry(request.target, completer));
|
||||
|
||||
try {
|
||||
final res = await _fetchActivity(request);
|
||||
if (!mounted) return;
|
||||
|
|
@ -324,28 +343,32 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
}
|
||||
}
|
||||
|
||||
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 is VocabMeaningPracticeActivityModel) {
|
||||
final choices = activityModel.multipleChoiceContent.choices.toList();
|
||||
await _fetchLemmaInfo(activityModel.storageKey, choices);
|
||||
}
|
||||
|
||||
return result.result!;
|
||||
return activityModel;
|
||||
}
|
||||
|
||||
Future<void> _fetchLemmaInfo(
|
||||
PracticeTarget target,
|
||||
String requestKey,
|
||||
List<String> choiceIds,
|
||||
) async {
|
||||
final texts = <String, String>{};
|
||||
|
|
@ -365,53 +388,35 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
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);
|
||||
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: ConstructTypeEnum.vocab,
|
||||
metadata: ConstructUseMetaData(
|
||||
roomId: null,
|
||||
timeStamp: DateTime.now(),
|
||||
),
|
||||
category: activity.targetTokens.first.pos,
|
||||
lemma: activity.targetTokens.first.lemma.text,
|
||||
form: activity.targetTokens.first.lemma.text,
|
||||
xp: useType.pointValue,
|
||||
PracticeRecordController.onSelectChoice(
|
||||
choiceContent,
|
||||
activity.tokens.first,
|
||||
activity,
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
TtsController.tryToSpeak(
|
||||
activity.targetTokens.first.vocabConstructID.lemma,
|
||||
langCode: MatrixState.pangeaController.userController.userL2!.langCode,
|
||||
);
|
||||
_playAudio();
|
||||
|
||||
// Display the fact that the choice was correct before loading the next activity
|
||||
await Future.delayed(const Duration(milliseconds: 1000));
|
||||
|
|
@ -425,11 +430,26 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -437,5 +457,5 @@ class VocabPracticeState extends State<VocabPractice> with AnalyticsUpdater {
|
|||
_analyticsService.derivedData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => VocabPracticeView(this);
|
||||
Widget build(BuildContext context) => AnalyticsPracticeView(this);
|
||||
}
|
||||
|
|
@ -2,35 +2,28 @@ import 'dart:math';
|
|||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constants.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_constants.dart';
|
||||
|
||||
class VocabPracticeSessionModel {
|
||||
class AnalyticsPracticeSessionModel {
|
||||
final DateTime startedAt;
|
||||
final List<PracticeTarget> practiceTargets;
|
||||
final String userL1;
|
||||
final String userL2;
|
||||
|
||||
VocabPracticeSessionState state;
|
||||
AnalyticsPracticeSessionState state;
|
||||
|
||||
VocabPracticeSessionModel({
|
||||
AnalyticsPracticeSessionModel({
|
||||
required this.startedAt,
|
||||
required this.practiceTargets,
|
||||
required this.userL1,
|
||||
required this.userL2,
|
||||
VocabPracticeSessionState? state,
|
||||
}) : assert(
|
||||
practiceTargets.every(
|
||||
(t) => {ActivityTypeEnum.lemmaMeaning, ActivityTypeEnum.lemmaAudio}
|
||||
.contains(t.activityType),
|
||||
),
|
||||
),
|
||||
state = state ?? const VocabPracticeSessionState();
|
||||
AnalyticsPracticeSessionState? state,
|
||||
}) : state = state ?? const AnalyticsPracticeSessionState();
|
||||
|
||||
int get _availableActivities => min(
|
||||
VocabPracticeConstants.practiceGroupSize,
|
||||
AnalyticsPracticeConstants.practiceGroupSize,
|
||||
practiceTargets.length,
|
||||
);
|
||||
|
||||
|
|
@ -45,9 +38,7 @@ class VocabPracticeSessionModel {
|
|||
userL1: userL1,
|
||||
userL2: userL2,
|
||||
activityQualityFeedback: null,
|
||||
targetTokens: target.tokens,
|
||||
targetType: target.activityType,
|
||||
targetMorphFeature: null,
|
||||
target: target,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
|
@ -64,8 +55,8 @@ class VocabPracticeSessionModel {
|
|||
completedUses: [...state.completedUses, use],
|
||||
);
|
||||
|
||||
factory VocabPracticeSessionModel.fromJson(Map<String, dynamic> json) {
|
||||
return VocabPracticeSessionModel(
|
||||
factory AnalyticsPracticeSessionModel.fromJson(Map<String, dynamic> json) {
|
||||
return AnalyticsPracticeSessionModel(
|
||||
startedAt: DateTime.parse(json['startedAt'] as String),
|
||||
practiceTargets: (json['practiceTargets'] as List<dynamic>)
|
||||
.map((e) => PracticeTarget.fromJson(e))
|
||||
|
|
@ -73,7 +64,7 @@ class VocabPracticeSessionModel {
|
|||
.toList(),
|
||||
userL1: json['userL1'] as String,
|
||||
userL2: json['userL2'] as String,
|
||||
state: VocabPracticeSessionState.fromJson(
|
||||
state: AnalyticsPracticeSessionState.fromJson(
|
||||
json,
|
||||
),
|
||||
);
|
||||
|
|
@ -90,13 +81,13 @@ class VocabPracticeSessionModel {
|
|||
}
|
||||
}
|
||||
|
||||
class VocabPracticeSessionState {
|
||||
class AnalyticsPracticeSessionState {
|
||||
final List<OneConstructUse> completedUses;
|
||||
final int currentIndex;
|
||||
final bool finished;
|
||||
final int elapsedSeconds;
|
||||
|
||||
const VocabPracticeSessionState({
|
||||
const AnalyticsPracticeSessionState({
|
||||
this.completedUses = const [],
|
||||
this.currentIndex = 0,
|
||||
this.finished = false,
|
||||
|
|
@ -115,7 +106,7 @@ class VocabPracticeSessionState {
|
|||
bool get _giveAccuracyBonus => accuracy >= 100.0;
|
||||
|
||||
bool get _giveTimeBonus =>
|
||||
elapsedSeconds <= VocabPracticeConstants.timeForBonus;
|
||||
elapsedSeconds <= AnalyticsPracticeConstants.timeForBonus;
|
||||
|
||||
int get bonusXP => accuracyBonusXP + timeBonusXP;
|
||||
|
||||
|
|
@ -148,13 +139,13 @@ class VocabPracticeSessionState {
|
|||
xp: ConstructUseTypeEnum.bonus.pointValue,
|
||||
);
|
||||
|
||||
VocabPracticeSessionState copyWith({
|
||||
AnalyticsPracticeSessionState copyWith({
|
||||
List<OneConstructUse>? completedUses,
|
||||
int? currentIndex,
|
||||
bool? finished,
|
||||
int? elapsedSeconds,
|
||||
}) {
|
||||
return VocabPracticeSessionState(
|
||||
return AnalyticsPracticeSessionState(
|
||||
completedUses: completedUses ?? this.completedUses,
|
||||
currentIndex: currentIndex ?? this.currentIndex,
|
||||
finished: finished ?? this.finished,
|
||||
|
|
@ -171,8 +162,8 @@ class VocabPracticeSessionState {
|
|||
};
|
||||
}
|
||||
|
||||
factory VocabPracticeSessionState.fromJson(Map<String, dynamic> json) {
|
||||
return VocabPracticeSessionState(
|
||||
factory AnalyticsPracticeSessionState.fromJson(Map<String, dynamic> json) {
|
||||
return AnalyticsPracticeSessionState(
|
||||
completedUses: (json['completedUses'] as List<dynamic>?)
|
||||
?.map((e) => OneConstructUse.fromJson(e))
|
||||
.whereType<OneConstructUse>()
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.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/constructs/construct_identifier.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/lemmas/lemma.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_target.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class AnalyticsPracticeSessionRepo {
|
||||
static final GetStorage _storage = GetStorage('practice_session');
|
||||
|
||||
static Future<AnalyticsPracticeSessionModel> get(
|
||||
ConstructTypeEnum type,
|
||||
) async {
|
||||
final cached = _getCached(type);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
final r = Random();
|
||||
final activityTypes = ActivityTypeEnum.analyticsPracticeTypes(type);
|
||||
|
||||
final types = List.generate(
|
||||
AnalyticsPracticeConstants.practiceGroupSize,
|
||||
(_) => activityTypes[r.nextInt(activityTypes.length)],
|
||||
);
|
||||
|
||||
final List<PracticeTarget> targets = [];
|
||||
|
||||
if (type == ConstructTypeEnum.vocab) {
|
||||
final constructs = await _fetchVocab();
|
||||
final targetCount = min(constructs.length, types.length);
|
||||
targets.addAll([
|
||||
for (var i = 0; i < targetCount; i++)
|
||||
PracticeTarget(
|
||||
tokens: [constructs[i].asToken],
|
||||
activityType: types[i],
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
final morphs = await _fetchMorphs();
|
||||
targets.addAll([
|
||||
for (final entry in morphs.entries)
|
||||
PracticeTarget(
|
||||
tokens: [entry.key],
|
||||
activityType: types[targets.length],
|
||||
morphFeature: entry.value,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
final session = AnalyticsPracticeSessionModel(
|
||||
userL1: MatrixState.pangeaController.userController.userL1!.langCode,
|
||||
userL2: MatrixState.pangeaController.userController.userL2!.langCode,
|
||||
startedAt: DateTime.now(),
|
||||
practiceTargets: targets,
|
||||
);
|
||||
await _setCached(type, session);
|
||||
return session;
|
||||
}
|
||||
|
||||
static Future<void> update(
|
||||
ConstructTypeEnum type,
|
||||
AnalyticsPracticeSessionModel session,
|
||||
) =>
|
||||
_setCached(type, session);
|
||||
|
||||
static Future<void> clear() => _storage.erase();
|
||||
|
||||
static Future<List<ConstructIdentifier>> _fetchVocab() async {
|
||||
final constructs = await MatrixState
|
||||
.pangeaController.matrixState.analyticsDataService
|
||||
.getAggregatedConstructs(ConstructTypeEnum.vocab)
|
||||
.then((map) => map.values.toList());
|
||||
|
||||
// sort by last used descending, nulls first
|
||||
constructs.sort((a, b) {
|
||||
final dateA = a.lastUsed;
|
||||
final dateB = b.lastUsed;
|
||||
if (dateA == null && dateB == null) return 0;
|
||||
if (dateA == null) return -1;
|
||||
if (dateB == null) return 1;
|
||||
return dateA.compareTo(dateB);
|
||||
});
|
||||
|
||||
final Set<String> seemLemmas = {};
|
||||
final targets = <ConstructIdentifier>[];
|
||||
for (final construct in constructs) {
|
||||
if (seemLemmas.contains(construct.lemma)) continue;
|
||||
seemLemmas.add(construct.lemma);
|
||||
targets.add(construct.id);
|
||||
if (targets.length >= AnalyticsPracticeConstants.practiceGroupSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
static Future<Map<PangeaToken, MorphFeaturesEnum>> _fetchMorphs() async {
|
||||
final constructs = await MatrixState
|
||||
.pangeaController.matrixState.analyticsDataService
|
||||
.getAggregatedConstructs(ConstructTypeEnum.morph)
|
||||
.then((map) => map.values.toList());
|
||||
|
||||
// sort by last used descending, nulls first
|
||||
constructs.sort((a, b) {
|
||||
final dateA = a.lastUsed;
|
||||
final dateB = b.lastUsed;
|
||||
if (dateA == null && dateB == null) return 0;
|
||||
if (dateA == null) return -1;
|
||||
if (dateB == null) return 1;
|
||||
return dateA.compareTo(dateB);
|
||||
});
|
||||
|
||||
final targets = <PangeaToken, MorphFeaturesEnum>{};
|
||||
final Set<String> seenForms = {};
|
||||
|
||||
for (final entry in constructs) {
|
||||
if (targets.length >= AnalyticsPracticeConstants.practiceGroupSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
final feature = MorphFeaturesEnumExtension.fromString(entry.id.category);
|
||||
if (feature == MorphFeaturesEnum.Unknown) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final use in entry.cappedUses) {
|
||||
if (targets.length >= AnalyticsPracticeConstants.practiceGroupSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (use.lemma.isEmpty) continue;
|
||||
final form = use.form;
|
||||
if (seenForms.contains(form) || form == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenForms.add(form);
|
||||
final token = PangeaToken(
|
||||
lemma: Lemma(
|
||||
text: form,
|
||||
saveVocab: true,
|
||||
form: form,
|
||||
),
|
||||
text: PangeaTokenText.fromString(form),
|
||||
pos: 'other',
|
||||
morph: {feature: use.lemma},
|
||||
);
|
||||
targets[token] = feature;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
static AnalyticsPracticeSessionModel? _getCached(
|
||||
ConstructTypeEnum type,
|
||||
) {
|
||||
try {
|
||||
final entry = _storage.read(type.name);
|
||||
if (entry == null) return null;
|
||||
final json = entry as Map<String, dynamic>;
|
||||
return AnalyticsPracticeSessionModel.fromJson(json);
|
||||
} catch (e) {
|
||||
_storage.remove(type.name);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _setCached(
|
||||
ConstructTypeEnum type,
|
||||
AnalyticsPracticeSessionModel session,
|
||||
) async {
|
||||
await _storage.write(
|
||||
type.name,
|
||||
session.toJson(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,15 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_page.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/choice_cards/audio_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/choice_cards/game_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/choice_cards/grammar_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/choice_cards/meaning_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/completed_activity_session_view.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/practice_timer_widget.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/animated_progress_bar.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/async_state.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
|
||||
|
|
@ -10,20 +19,13 @@ import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'
|
|||
import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.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/vocab_practice/choice_cards/audio_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/choice_cards/game_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/choice_cards/meaning_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/completed_activity_session_view.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_page.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_timer_widget.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class VocabPracticeView extends StatelessWidget {
|
||||
final VocabPracticeState controller;
|
||||
class AnalyticsPracticeView extends StatelessWidget {
|
||||
final AnalyticsPracticeState controller;
|
||||
|
||||
const VocabPracticeView(this.controller, {super.key});
|
||||
const AnalyticsPracticeView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -55,8 +57,8 @@ class VocabPracticeView extends StatelessWidget {
|
|||
ValueListenableBuilder(
|
||||
valueListenable: controller.sessionState,
|
||||
builder: (context, state, __) {
|
||||
if (state is AsyncLoaded<VocabPracticeSessionModel>) {
|
||||
return VocabTimerWidget(
|
||||
if (state is AsyncLoaded<AnalyticsPracticeSessionModel>) {
|
||||
return PracticeTimerWidget(
|
||||
key: ValueKey(state.value.startedAt),
|
||||
initialSeconds: state.value.state.elapsedSeconds,
|
||||
onTimeUpdate: controller.updateElapsedTime,
|
||||
|
|
@ -81,12 +83,12 @@ class VocabPracticeView extends StatelessWidget {
|
|||
valueListenable: controller.sessionState,
|
||||
builder: (context, state, __) {
|
||||
return switch (state) {
|
||||
AsyncError<VocabPracticeSessionModel>(:final error) =>
|
||||
AsyncError<AnalyticsPracticeSessionModel>(:final error) =>
|
||||
ErrorIndicator(message: error.toString()),
|
||||
AsyncLoaded<VocabPracticeSessionModel>(:final value) =>
|
||||
AsyncLoaded<AnalyticsPracticeSessionModel>(:final value) =>
|
||||
value.isComplete
|
||||
? CompletedActivitySessionView(state.value, controller)
|
||||
: _VocabActivityView(controller),
|
||||
: _AnalyticsActivityView(controller),
|
||||
_ => loading,
|
||||
};
|
||||
},
|
||||
|
|
@ -97,10 +99,10 @@ class VocabPracticeView extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _VocabActivityView extends StatelessWidget {
|
||||
final VocabPracticeState controller;
|
||||
class _AnalyticsActivityView extends StatelessWidget {
|
||||
final AnalyticsPracticeState controller;
|
||||
|
||||
const _VocabActivityView(
|
||||
const _AnalyticsActivityView(
|
||||
this.controller,
|
||||
);
|
||||
|
||||
|
|
@ -123,13 +125,13 @@ class _VocabActivityView extends StatelessWidget {
|
|||
Expanded(
|
||||
flex: 1,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller.activityConstructId,
|
||||
builder: (context, constructId, __) => constructId != null
|
||||
valueListenable: controller.activityTarget,
|
||||
builder: (context, target, __) => target != null
|
||||
? Column(
|
||||
spacing: 12.0,
|
||||
children: [
|
||||
Text(
|
||||
constructId.lemma,
|
||||
target.promptText(context),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
|
|
@ -138,12 +140,15 @@ class _VocabActivityView extends StatelessWidget {
|
|||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
PhoneticTranscriptionWidget(
|
||||
text: constructId.lemma,
|
||||
textLanguage: MatrixState
|
||||
.pangeaController.userController.userL2!,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
),
|
||||
if (controller.widget.type ==
|
||||
ConstructTypeEnum.vocab)
|
||||
PhoneticTranscriptionWidget(
|
||||
text:
|
||||
target.tokens.first.vocabConstructID.lemma,
|
||||
textLanguage: MatrixState
|
||||
.pangeaController.userController.userL2!,
|
||||
style: const TextStyle(fontSize: 14.0),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox(),
|
||||
|
|
@ -153,10 +158,10 @@ class _VocabActivityView extends StatelessWidget {
|
|||
flex: 2,
|
||||
child: Center(
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: controller.activityConstructId,
|
||||
builder: (context, constructId, __) => constructId != null
|
||||
valueListenable: controller.activityTarget,
|
||||
builder: (context, target, __) => target != null
|
||||
? _ExampleMessageWidget(
|
||||
controller.getExampleMessage(constructId),
|
||||
controller.getExampleMessage(target),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
|
|
@ -216,7 +221,7 @@ class _ExampleMessageWidget extends StatelessWidget {
|
|||
}
|
||||
|
||||
class _ActivityChoicesWidget extends StatelessWidget {
|
||||
final VocabPracticeState controller;
|
||||
final AnalyticsPracticeState controller;
|
||||
|
||||
const _ActivityChoicesWidget(
|
||||
this.controller,
|
||||
|
|
@ -228,14 +233,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
|
||||
|
|
@ -248,12 +254,10 @@ 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!,
|
||||
);
|
||||
final choices = controller.filteredChoices(value);
|
||||
final constrainedHeight =
|
||||
constraints.maxHeight.clamp(0.0, 400.0);
|
||||
final cardHeight = (constrainedHeight / (choices.length + 1))
|
||||
|
|
@ -272,7 +276,6 @@ class _ActivityChoicesWidget extends StatelessWidget {
|
|||
controller.choiceTargetId(choice.choiceId),
|
||||
choiceId: choice.choiceId,
|
||||
onPressed: () => controller.onSelectChoice(
|
||||
value.targetTokens.first.vocabConstructID,
|
||||
choice.choiceId,
|
||||
),
|
||||
cardHeight: cardHeight,
|
||||
|
|
@ -298,7 +301,7 @@ class _ActivityChoicesWidget extends StatelessWidget {
|
|||
}
|
||||
|
||||
class _ChoiceCard extends StatelessWidget {
|
||||
final PracticeActivityModel activity;
|
||||
final MultipleChoicePracticeActivityModel activity;
|
||||
final String choiceId;
|
||||
final String targetId;
|
||||
final VoidCallback onPressed;
|
||||
|
|
@ -319,9 +322,9 @@ 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;
|
||||
final constructId = activity.tokens.first.vocabConstructID;
|
||||
|
||||
switch (activity.activityType) {
|
||||
case ActivityTypeEnum.lemmaMeaning:
|
||||
|
|
@ -350,6 +353,19 @@ class _ChoiceCard extends StatelessWidget {
|
|||
height: cardHeight,
|
||||
);
|
||||
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return GrammarChoiceCard(
|
||||
key: ValueKey(
|
||||
'${constructId.string}_${activityType.name}_grammar_$choiceId',
|
||||
),
|
||||
choiceId: choiceId,
|
||||
targetId: targetId,
|
||||
feature: (activity as MorphPracticeActivityModel).morphFeature,
|
||||
tag: choiceText,
|
||||
onPressed: onPressed,
|
||||
isCorrect: isCorrect,
|
||||
);
|
||||
|
||||
default:
|
||||
return GameChoiceCard(
|
||||
key: ValueKey(
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/choice_cards/game_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/common/widgets/word_audio_button.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/choice_cards/game_choice_card.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
/// Displays an audio button with a select label in a row layout
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_practice/choice_cards/game_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
|
||||
|
||||
/// Choice card for meaning activity with emoji, and alt text on flip
|
||||
class GrammarChoiceCard extends StatelessWidget {
|
||||
final String choiceId;
|
||||
final String targetId;
|
||||
|
||||
final MorphFeaturesEnum feature;
|
||||
final String tag;
|
||||
|
||||
final VoidCallback onPressed;
|
||||
final bool isCorrect;
|
||||
final double height;
|
||||
|
||||
const GrammarChoiceCard({
|
||||
required this.choiceId,
|
||||
required this.targetId,
|
||||
required this.feature,
|
||||
required this.tag,
|
||||
required this.onPressed,
|
||||
required this.isCorrect,
|
||||
this.height = 72.0,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final copy = getGrammarCopy(
|
||||
category: feature.name,
|
||||
lemma: tag,
|
||||
context: context,
|
||||
) ??
|
||||
tag;
|
||||
|
||||
return GameChoiceCard(
|
||||
shouldFlip: false,
|
||||
targetId: targetId,
|
||||
onPressed: onPressed,
|
||||
isCorrect: isCorrect,
|
||||
height: height,
|
||||
child: Text(copy),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_practice/choice_cards/game_choice_card.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/choice_cards/game_choice_card.dart';
|
||||
|
||||
/// Choice card for meaning activity with emoji, and alt text on flip
|
||||
class MeaningChoiceCard extends StatelessWidget {
|
||||
|
|
@ -3,18 +3,18 @@ import 'package:flutter/material.dart';
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constants.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_page.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/percent_marker_bar.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/stat_card.dart';
|
||||
import 'package:fluffychat/pangea/analytics_summary/animated_progress_bar.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/percent_marker_bar.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/stat_card.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_constants.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_page.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_session_model.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class CompletedActivitySessionView extends StatelessWidget {
|
||||
final VocabPracticeSessionModel session;
|
||||
final VocabPracticeState controller;
|
||||
final AnalyticsPracticeSessionModel session;
|
||||
final AnalyticsPracticeState controller;
|
||||
const CompletedActivitySessionView(
|
||||
this.session,
|
||||
this.controller, {
|
||||
|
|
@ -198,7 +198,7 @@ class TimeStarsWidget extends StatelessWidget {
|
|||
});
|
||||
|
||||
int get starCount {
|
||||
const timeForBonus = VocabPracticeConstants.timeForBonus;
|
||||
const timeForBonus = AnalyticsPracticeConstants.timeForBonus;
|
||||
if (elapsedSeconds <= timeForBonus) return 5;
|
||||
if (elapsedSeconds <= timeForBonus * 1.5) return 4;
|
||||
if (elapsedSeconds <= timeForBonus * 2) return 3;
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/morphs/default_morph_mapping.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_models.dart';
|
||||
import 'package:fluffychat/pangea/morphs/morph_repo.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/widgets/matrix.dart';
|
||||
|
||||
class MorphCategoryActivityGenerator {
|
||||
static Future<MessageActivityResponse> get(
|
||||
MessageActivityRequest req,
|
||||
) async {
|
||||
if (req.target.morphFeature == null) {
|
||||
throw ArgumentError(
|
||||
"MorphCategoryActivityGenerator requires a targetMorphFeature",
|
||||
);
|
||||
}
|
||||
|
||||
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",
|
||||
);
|
||||
}
|
||||
|
||||
MorphFeaturesAndTags morphs = defaultMorphMapping;
|
||||
|
||||
try {
|
||||
final resp = await MorphsRepo.get();
|
||||
morphs = resp;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {"l2": MatrixState.pangeaController.userController.userL2},
|
||||
);
|
||||
}
|
||||
|
||||
final List<String> allTags = morphs.getDisplayTags(feature.name);
|
||||
final List<String> possibleDistractors = allTags
|
||||
.where(
|
||||
(tag) => tag.toLowerCase() != morphTag.toLowerCase() && tag != "X",
|
||||
)
|
||||
.toList();
|
||||
|
||||
final choices = possibleDistractors.take(3).toList();
|
||||
choices.add(morphTag);
|
||||
choices.shuffle();
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: MorphCategoryPracticeActivityModel(
|
||||
tokens: req.target.tokens,
|
||||
langCode: req.userL2,
|
||||
morphFeature: feature,
|
||||
multipleChoiceContent: MultipleChoiceActivity(
|
||||
choices: choices.toSet(),
|
||||
answers: {morphTag},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,12 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class VocabTimerWidget extends StatefulWidget {
|
||||
class PracticeTimerWidget extends StatefulWidget {
|
||||
final int initialSeconds;
|
||||
final ValueChanged<int> onTimeUpdate;
|
||||
final bool isRunning;
|
||||
|
||||
const VocabTimerWidget({
|
||||
const PracticeTimerWidget({
|
||||
required this.initialSeconds,
|
||||
required this.onTimeUpdate,
|
||||
this.isRunning = true,
|
||||
|
|
@ -15,10 +15,10 @@ class VocabTimerWidget extends StatefulWidget {
|
|||
});
|
||||
|
||||
@override
|
||||
VocabTimerWidgetState createState() => VocabTimerWidgetState();
|
||||
PracticeTimerWidgetState createState() => PracticeTimerWidgetState();
|
||||
}
|
||||
|
||||
class VocabTimerWidgetState extends State<VocabTimerWidget> {
|
||||
class PracticeTimerWidgetState extends State<PracticeTimerWidget> {
|
||||
final Stopwatch _stopwatch = Stopwatch();
|
||||
late int _initialSeconds;
|
||||
Timer? _timer;
|
||||
|
|
@ -33,7 +33,7 @@ class VocabTimerWidgetState extends State<VocabTimerWidget> {
|
|||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(VocabTimerWidget oldWidget) {
|
||||
void didUpdateWidget(PracticeTimerWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.isRunning && !widget.isRunning) {
|
||||
_stopTimer();
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
@ -15,9 +15,8 @@ class VocabAudioActivityGenerator {
|
|||
choicesList.shuffle();
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activityType: req.targetType,
|
||||
targetTokens: [token],
|
||||
activity: VocabAudioPracticeActivityModel(
|
||||
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);
|
||||
|
||||
|
|
@ -18,9 +18,8 @@ class VocabMeaningActivityGenerator {
|
|||
final Set<String> constructIdChoices = choices.map((c) => c.string).toSet();
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activityType: req.targetType,
|
||||
targetTokens: [token],
|
||||
activity: VocabMeaningPracticeActivityModel(
|
||||
tokens: req.target.tokens,
|
||||
langCode: req.userL2,
|
||||
multipleChoiceContent: MultipleChoiceActivity(
|
||||
choices: constructIdChoices,
|
||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
|
||||
enum ActivityTypeEnum {
|
||||
|
|
@ -13,7 +14,8 @@ enum ActivityTypeEnum {
|
|||
morphId,
|
||||
messageMeaning,
|
||||
lemmaMeaning,
|
||||
lemmaAudio;
|
||||
lemmaAudio,
|
||||
grammarCategory;
|
||||
|
||||
bool get includeTTSOnClick {
|
||||
switch (this) {
|
||||
|
|
@ -27,6 +29,7 @@ enum ActivityTypeEnum {
|
|||
case ActivityTypeEnum.hiddenWordListening:
|
||||
case ActivityTypeEnum.lemmaAudio:
|
||||
case ActivityTypeEnum.lemmaMeaning:
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -62,6 +65,9 @@ enum ActivityTypeEnum {
|
|||
case 'lemma_audio':
|
||||
case 'lemmaAudio':
|
||||
return ActivityTypeEnum.lemmaAudio;
|
||||
case 'grammar_category':
|
||||
case 'grammarCategory':
|
||||
return ActivityTypeEnum.grammarCategory;
|
||||
default:
|
||||
throw Exception('Unknown activity type: $split');
|
||||
}
|
||||
|
|
@ -117,6 +123,11 @@ enum ActivityTypeEnum {
|
|||
ConstructUseTypeEnum.corLM,
|
||||
ConstructUseTypeEnum.incLM,
|
||||
];
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return [
|
||||
ConstructUseTypeEnum.corGC,
|
||||
ConstructUseTypeEnum.incGC,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,6 +151,8 @@ enum ActivityTypeEnum {
|
|||
return ConstructUseTypeEnum.corLA;
|
||||
case ActivityTypeEnum.lemmaMeaning:
|
||||
return ConstructUseTypeEnum.corLM;
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return ConstructUseTypeEnum.corGC;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +176,8 @@ enum ActivityTypeEnum {
|
|||
return ConstructUseTypeEnum.incLA;
|
||||
case ActivityTypeEnum.lemmaMeaning:
|
||||
return ConstructUseTypeEnum.incLM;
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return ConstructUseTypeEnum.incGC;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -182,6 +197,7 @@ enum ActivityTypeEnum {
|
|||
case ActivityTypeEnum.morphId:
|
||||
return Icons.format_shapes;
|
||||
case ActivityTypeEnum.messageMeaning:
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return Icons.star; // TODO: Add to L10n
|
||||
}
|
||||
}
|
||||
|
|
@ -200,6 +216,7 @@ enum ActivityTypeEnum {
|
|||
case ActivityTypeEnum.messageMeaning:
|
||||
case ActivityTypeEnum.lemmaMeaning:
|
||||
case ActivityTypeEnum.lemmaAudio:
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -210,4 +227,24 @@ enum ActivityTypeEnum {
|
|||
ActivityTypeEnum.wordFocusListening,
|
||||
ActivityTypeEnum.morphId,
|
||||
];
|
||||
|
||||
static List<ActivityTypeEnum> get _vocabPracticeTypes => [
|
||||
ActivityTypeEnum.lemmaMeaning,
|
||||
// ActivityTypeEnum.lemmaAudio,
|
||||
];
|
||||
|
||||
static List<ActivityTypeEnum> get _grammarPracticeTypes => [
|
||||
ActivityTypeEnum.grammarCategory,
|
||||
];
|
||||
|
||||
static List<ActivityTypeEnum> analyticsPracticeTypes(
|
||||
ConstructTypeEnum constructType,
|
||||
) {
|
||||
switch (constructType) {
|
||||
case ConstructTypeEnum.vocab:
|
||||
return _vocabPracticeTypes;
|
||||
case ConstructTypeEnum.morph:
|
||||
return _grammarPracticeTypes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -13,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");
|
||||
}
|
||||
|
||||
|
|
@ -28,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,9 +64,8 @@ class EmojiActivityGenerator {
|
|||
}
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activityType: ActivityTypeEnum.emoji,
|
||||
targetTokens: req.targetTokens,
|
||||
activity: EmojiPracticeActivityModel(
|
||||
tokens: req.target.tokens,
|
||||
langCode: req.userL2,
|
||||
matchContent: PracticeMatchActivity(
|
||||
matchInfo: matchInfo,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -15,16 +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: PracticeActivityModel(
|
||||
activityType: ActivityTypeEnum.lemmaId,
|
||||
targetTokens: [token],
|
||||
activity: LemmaPracticeActivityModel(
|
||||
tokens: req.target.tokens,
|
||||
langCode: req.userL2,
|
||||
multipleChoiceContent: MultipleChoiceActivity(
|
||||
choices: choices.map((c) => c.lemma).toSet(),
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -16,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();
|
||||
|
||||
|
|
@ -28,14 +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: PracticeActivityModel(
|
||||
activityType: ActivityTypeEnum.wordMeaning,
|
||||
targetTokens: req.targetTokens,
|
||||
activity: LemmaMeaningPracticeActivityModel(
|
||||
tokens: req.target.tokens,
|
||||
langCode: req.userL2,
|
||||
matchContent: PracticeMatchActivity(
|
||||
matchInfo: matchInfo,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
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';
|
||||
|
||||
// includes feedback text and the bad activity model
|
||||
class ActivityQualityFeedback {
|
||||
|
|
@ -16,15 +13,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,
|
||||
|
|
@ -50,22 +38,16 @@ 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');
|
||||
}
|
||||
}
|
||||
|
|
@ -75,9 +57,9 @@ class MessageActivityRequest {
|
|||
'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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -86,19 +68,19 @@ class MessageActivityRequest {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -18,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,11 +37,10 @@ class MorphActivityGenerator {
|
|||
debugger(when: kDebugMode && distractors.length < 3);
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
targetTokens: req.targetTokens,
|
||||
activity: MorphMatchPracticeActivityModel(
|
||||
tokens: req.target.tokens,
|
||||
langCode: req.userL2,
|
||||
activityType: ActivityTypeEnum.morphId,
|
||||
morphFeature: req.targetMorphFeature,
|
||||
morphFeature: morphFeature,
|
||||
multipleChoiceContent: MultipleChoiceActivity(
|
||||
choices: distractors,
|
||||
answers: {morphTag},
|
||||
|
|
|
|||
|
|
@ -1,152 +1,57 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.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/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';
|
||||
|
||||
class PracticeActivityModel {
|
||||
final List<PangeaToken> targetTokens;
|
||||
final ActivityTypeEnum activityType;
|
||||
final MorphFeaturesEnum? morphFeature;
|
||||
|
||||
sealed class PracticeActivityModel {
|
||||
final List<PangeaToken> tokens;
|
||||
final String langCode;
|
||||
|
||||
final MultipleChoiceActivity? multipleChoiceContent;
|
||||
final PracticeMatchActivity? matchContent;
|
||||
|
||||
PracticeActivityModel({
|
||||
required this.targetTokens,
|
||||
const PracticeActivityModel({
|
||||
required this.tokens,
|
||||
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 storageKey =>
|
||||
'${activityType.name}-${tokens.map((e) => e.text.content).join("-")}';
|
||||
|
||||
PracticeTarget get practiceTarget => PracticeTarget(
|
||||
tokens: targetTokens,
|
||||
activityType: activityType,
|
||||
morphFeature: morphFeature,
|
||||
tokens: tokens,
|
||||
morphFeature: this is MorphPracticeActivityModel
|
||||
? (this as MorphPracticeActivityModel).morphFeature
|
||||
: null,
|
||||
);
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
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}),
|
||||
|
|
@ -163,58 +68,308 @@ 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,
|
||||
tokens: tokens,
|
||||
morphFeature: morph!,
|
||||
multipleChoiceContent: multipleChoiceContent!,
|
||||
);
|
||||
case ActivityTypeEnum.lemmaAudio:
|
||||
assert(
|
||||
multipleChoiceContent != null,
|
||||
"multipleChoiceContent is null in PracticeActivityModel.fromJson for lemmaAudio",
|
||||
);
|
||||
return VocabAudioPracticeActivityModel(
|
||||
langCode: langCode,
|
||||
tokens: tokens,
|
||||
multipleChoiceContent: multipleChoiceContent!,
|
||||
);
|
||||
case ActivityTypeEnum.lemmaMeaning:
|
||||
assert(
|
||||
multipleChoiceContent != null,
|
||||
"multipleChoiceContent is null in PracticeActivityModel.fromJson for lemmaMeaning",
|
||||
);
|
||||
return VocabMeaningPracticeActivityModel(
|
||||
langCode: langCode,
|
||||
tokens: tokens,
|
||||
multipleChoiceContent: multipleChoiceContent!,
|
||||
);
|
||||
case ActivityTypeEnum.emoji:
|
||||
assert(
|
||||
matchContent != null,
|
||||
"matchContent is null in PracticeActivityModel.fromJson for emoji",
|
||||
);
|
||||
return EmojiPracticeActivityModel(
|
||||
langCode: langCode,
|
||||
tokens: tokens,
|
||||
matchContent: matchContent!,
|
||||
);
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
assert(
|
||||
multipleChoiceContent != null,
|
||||
"multipleChoiceContent is null in PracticeActivityModel.fromJson for lemmaId",
|
||||
);
|
||||
return LemmaPracticeActivityModel(
|
||||
langCode: langCode,
|
||||
tokens: tokens,
|
||||
multipleChoiceContent: multipleChoiceContent!,
|
||||
);
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
assert(
|
||||
matchContent != null,
|
||||
"matchContent is null in PracticeActivityModel.fromJson for wordMeaning",
|
||||
);
|
||||
return LemmaMeaningPracticeActivityModel(
|
||||
langCode: langCode,
|
||||
tokens: 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,
|
||||
tokens: tokens,
|
||||
morphFeature: morph!,
|
||||
multipleChoiceContent: multipleChoiceContent!,
|
||||
);
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
assert(
|
||||
matchContent != null,
|
||||
"matchContent is null in PracticeActivityModel.fromJson for wordFocusListening",
|
||||
);
|
||||
return WordListeningPracticeActivityModel(
|
||||
langCode: langCode,
|
||||
tokens: 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,
|
||||
'target_tokens': tokens.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 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.tokens,
|
||||
required super.langCode,
|
||||
required this.multipleChoiceContent,
|
||||
});
|
||||
|
||||
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,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
metadata: ConstructUseMetaData(
|
||||
roomId: null,
|
||||
timeStamp: DateTime.now(),
|
||||
),
|
||||
category: token.pos,
|
||||
lemma: token.lemma.text,
|
||||
form: token.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.tokens,
|
||||
required super.langCode,
|
||||
required this.matchContent,
|
||||
});
|
||||
|
||||
bool isCorrect(
|
||||
PangeaToken token,
|
||||
String choice,
|
||||
) =>
|
||||
matchContent.matchInfo[token.vocabForm]!.contains(choice);
|
||||
|
||||
@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.tokens,
|
||||
required super.langCode,
|
||||
required super.multipleChoiceContent,
|
||||
required this.morphFeature,
|
||||
});
|
||||
|
||||
@override
|
||||
String get storageKey =>
|
||||
'${activityType.name}-${tokens.map((e) => e.text.content).join("-")}-${morphFeature.name}';
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = super.toJson();
|
||||
json['morph_feature'] = morphFeature.name;
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
class MorphCategoryPracticeActivityModel extends MorphPracticeActivityModel {
|
||||
MorphCategoryPracticeActivityModel({
|
||||
required super.tokens,
|
||||
required super.langCode,
|
||||
required super.morphFeature,
|
||||
required super.multipleChoiceContent,
|
||||
});
|
||||
|
||||
@override
|
||||
OneConstructUse constructUse(String choiceContent) {
|
||||
final correct = multipleChoiceContent.isCorrect(choiceContent);
|
||||
final token = tokens.first;
|
||||
final useType =
|
||||
correct ? activityType.correctUse : activityType.incorrectUse;
|
||||
final tag = token.getMorphTag(morphFeature)!;
|
||||
|
||||
return OneConstructUse(
|
||||
useType: useType,
|
||||
constructType: ConstructTypeEnum.morph,
|
||||
metadata: ConstructUseMetaData(
|
||||
roomId: null,
|
||||
timeStamp: DateTime.now(),
|
||||
),
|
||||
category: morphFeature.name,
|
||||
lemma: tag,
|
||||
form: token.lemma.form,
|
||||
xp: useType.pointValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MorphMatchPracticeActivityModel extends MorphPracticeActivityModel {
|
||||
MorphMatchPracticeActivityModel({
|
||||
required super.tokens,
|
||||
required super.langCode,
|
||||
required super.morphFeature,
|
||||
required super.multipleChoiceContent,
|
||||
});
|
||||
}
|
||||
|
||||
class VocabAudioPracticeActivityModel
|
||||
extends MultipleChoicePracticeActivityModel {
|
||||
VocabAudioPracticeActivityModel({
|
||||
required super.tokens,
|
||||
required super.langCode,
|
||||
required super.multipleChoiceContent,
|
||||
});
|
||||
}
|
||||
|
||||
class VocabMeaningPracticeActivityModel
|
||||
extends MultipleChoicePracticeActivityModel {
|
||||
VocabMeaningPracticeActivityModel({
|
||||
required super.tokens,
|
||||
required super.langCode,
|
||||
required super.multipleChoiceContent,
|
||||
});
|
||||
}
|
||||
|
||||
class LemmaPracticeActivityModel extends MultipleChoicePracticeActivityModel {
|
||||
LemmaPracticeActivityModel({
|
||||
required super.tokens,
|
||||
required super.langCode,
|
||||
required super.multipleChoiceContent,
|
||||
});
|
||||
}
|
||||
|
||||
class EmojiPracticeActivityModel extends MatchPracticeActivityModel {
|
||||
EmojiPracticeActivityModel({
|
||||
required super.tokens,
|
||||
required super.langCode,
|
||||
required super.matchContent,
|
||||
});
|
||||
}
|
||||
|
||||
class LemmaMeaningPracticeActivityModel extends MatchPracticeActivityModel {
|
||||
LemmaMeaningPracticeActivityModel({
|
||||
required super.tokens,
|
||||
required super.langCode,
|
||||
required super.matchContent,
|
||||
});
|
||||
}
|
||||
|
||||
class WordListeningPracticeActivityModel extends MatchPracticeActivityModel {
|
||||
WordListeningPracticeActivityModel({
|
||||
required super.tokens,
|
||||
required super.langCode,
|
||||
required super.matchContent,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import 'package:async/async.dart';
|
|||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_practice/morph_category_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/vocab_audio_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/analytics_practice/vocab_meaning_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/network/requests.dart';
|
||||
import 'package:fluffychat/pangea/common/network/urls.dart';
|
||||
|
|
@ -21,8 +24,6 @@ import 'package:fluffychat/pangea/practice_activities/message_activity_request.d
|
|||
import 'package:fluffychat/pangea/practice_activities/morph_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/word_focus_listening_generator.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_audio_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_meaning_activity_generator.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
/// Represents an item in the completion cache.
|
||||
|
|
@ -116,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:
|
||||
|
|
@ -125,6 +126,8 @@ class PracticeRepo {
|
|||
return VocabMeaningActivityGenerator.get(req);
|
||||
case ActivityTypeEnum.lemmaAudio:
|
||||
return VocabAudioActivityGenerator.get(req);
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return MorphCategoryActivityGenerator.get(req);
|
||||
case ActivityTypeEnum.morphId:
|
||||
return MorphActivityGenerator.get(req);
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/l10n/l10n.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
|
||||
|
|
@ -89,79 +85,23 @@ class PracticeTarget {
|
|||
(morphFeature?.name ?? "");
|
||||
}
|
||||
|
||||
PracticeRecord get record => PracticeRecordRepo.get(this);
|
||||
|
||||
bool get isComplete {
|
||||
if (activityType == ActivityTypeEnum.morphId) {
|
||||
return record.completeResponses > 0;
|
||||
String promptText(BuildContext context) {
|
||||
switch (activityType) {
|
||||
case ActivityTypeEnum.grammarCategory:
|
||||
return L10n.of(context).whatIsTheMorphTag(
|
||||
morphFeature!.getDisplayCopy(context),
|
||||
tokens.first.text.content,
|
||||
);
|
||||
default:
|
||||
return tokens.first.vocabConstructID.lemma;
|
||||
}
|
||||
|
||||
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;
|
||||
ConstructIdentifier targetTokenConstructID(PangeaToken token) {
|
||||
final defaultID = token.vocabConstructID;
|
||||
final ConstructIdentifier? cId = morphFeature == null
|
||||
? defaultID
|
||||
: token.morphIdByFeature(morphFeature!);
|
||||
return cId ?? defaultID;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -8,20 +7,19 @@ 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",
|
||||
);
|
||||
}
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activityType: ActivityTypeEnum.wordFocusListening,
|
||||
targetTokens: req.targetTokens,
|
||||
activity: WordListeningPracticeActivityModel(
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -51,8 +51,8 @@ class MessageMorphInputBarContentState
|
|||
extends State<MessageMorphInputBarContent> {
|
||||
String? selectedTag;
|
||||
|
||||
PangeaToken get token => widget.activity.targetTokens.first;
|
||||
MorphFeaturesEnum get morph => widget.activity.morphFeature!;
|
||||
PangeaToken get token => widget.activity.tokens.first;
|
||||
MorphFeaturesEnum get morph => widget.activity.morphFeature;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MessageMorphInputBarContent oldWidget) {
|
||||
|
|
@ -114,10 +114,9 @@ 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);
|
||||
final wasCorrect = widget.controller.wasCorrectChoice(choice);
|
||||
|
||||
return ChoiceAnimationWidget(
|
||||
isSelected: selectedTag == choice,
|
||||
|
|
@ -135,9 +134,8 @@ class MessageMorphInputBarContentState
|
|||
PracticeChoice(
|
||||
choiceContent: choice,
|
||||
form: ConstructForm(
|
||||
cId: widget.activity.targetTokens.first
|
||||
.morphIdByFeature(
|
||||
widget.activity.morphFeature!,
|
||||
cId: widget.activity.tokens.first.morphIdByFeature(
|
||||
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(),
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
@ -35,18 +36,34 @@ class PracticeController with ChangeNotifier {
|
|||
MorphSelection? selectedMorph;
|
||||
PracticeChoice? selectedChoice;
|
||||
|
||||
PracticeActivityModel? get activity => _activity;
|
||||
|
||||
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;
|
||||
return PracticeRecordController.wasCorrectMatch(
|
||||
_activity!.practiceTarget,
|
||||
choice,
|
||||
);
|
||||
}
|
||||
|
||||
bool isPracticeActivityDone(ActivityTypeEnum activityType) =>
|
||||
practiceSelection?.activities(activityType).every((a) => a.isComplete) ==
|
||||
bool? wasCorrectChoice(String choice) {
|
||||
if (_activity == null) return false;
|
||||
return PracticeRecordController.wasCorrectChoice(
|
||||
_activity!.practiceTarget,
|
||||
choice,
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -66,23 +83,25 @@ class PracticeController with ChangeNotifier {
|
|||
}
|
||||
|
||||
return target == null ||
|
||||
target.isCompleteByToken(
|
||||
token,
|
||||
_activity?.morphFeature,
|
||||
) ==
|
||||
true;
|
||||
PracticeRecordController.isCompleteByToken(
|
||||
target,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
bool get showChoiceShimmer {
|
||||
if (_activity == null) return false;
|
||||
|
||||
if (_activity!.activityType == ActivityTypeEnum.morphId) {
|
||||
if (_activity is MorphMatchPracticeActivityModel) {
|
||||
return selectedMorph != null &&
|
||||
!_activity!.practiceTarget.hasAnyResponses;
|
||||
!PracticeRecordController.hasResponse(
|
||||
_activity!.practiceTarget,
|
||||
);
|
||||
}
|
||||
|
||||
return selectedChoice == null &&
|
||||
!_activity!.practiceTarget.hasAnyCorrectChoices;
|
||||
!PracticeRecordController.hasAnyCorrectChoices(
|
||||
_activity!.practiceTarget,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _fetchPracticeSelection() async {
|
||||
|
|
@ -101,9 +120,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(
|
||||
|
|
@ -151,11 +168,11 @@ 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 = PracticeRecordController.onSelectChoice(
|
||||
choice.choiceContent,
|
||||
token,
|
||||
_activity!,
|
||||
);
|
||||
|
||||
final targetId =
|
||||
"message-token-${token.text.uniqueKey}-${pangeaMessageEvent.eventId}";
|
||||
|
|
@ -164,9 +181,10 @@ 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 =
|
||||
PracticeRecordController.lastResponse(_activity!.practiceTarget)!
|
||||
.useType(_activity!.activityType);
|
||||
|
||||
final constructs = [
|
||||
OneConstructUse(
|
||||
|
|
@ -192,14 +210,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';
|
||||
|
|
@ -16,7 +12,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,18 +21,14 @@ class MatchActivityCard extends StatelessWidget {
|
|||
required this.controller,
|
||||
});
|
||||
|
||||
PracticeActivityModel get activity => currentActivity;
|
||||
|
||||
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(
|
||||
|
|
@ -45,7 +37,7 @@ class MatchActivityCard extends StatelessWidget {
|
|||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
case WordListeningPracticeActivityModel():
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(
|
||||
|
|
@ -53,9 +45,6 @@ class MatchActivityCard extends StatelessWidget {
|
|||
size: fontSize,
|
||||
),
|
||||
);
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,15 +72,14 @@ 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);
|
||||
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,
|
||||
|
|
@ -100,7 +88,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,119 @@
|
|||
import 'package:collection/collection.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/practice_activity_model.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';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
||||
|
||||
class PracticeRecordController {
|
||||
static PracticeRecord _recordByTarget(PracticeTarget target) =>
|
||||
PracticeRecordRepo.get(target);
|
||||
|
||||
static bool hasResponse(PracticeTarget target) =>
|
||||
_recordByTarget(target).responses.isNotEmpty;
|
||||
|
||||
static ActivityRecordResponse? lastResponse(PracticeTarget target) {
|
||||
final record = _recordByTarget(target);
|
||||
return record.responses.lastOrNull;
|
||||
}
|
||||
|
||||
static ActivityRecordResponse? correctResponse(
|
||||
PracticeTarget target,
|
||||
PangeaToken token,
|
||||
) {
|
||||
final record = _recordByTarget(target);
|
||||
return record.responses.firstWhereOrNull(
|
||||
(res) => res.cId == target.targetTokenConstructID(token) && res.isCorrect,
|
||||
);
|
||||
}
|
||||
|
||||
static bool? wasCorrectMatch(
|
||||
PracticeTarget target,
|
||||
PracticeChoice choice,
|
||||
) {
|
||||
final record = _recordByTarget(target);
|
||||
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;
|
||||
}
|
||||
|
||||
static bool? wasCorrectChoice(
|
||||
PracticeTarget target,
|
||||
String choice,
|
||||
) {
|
||||
final record = _recordByTarget(target);
|
||||
for (final response in record.responses) {
|
||||
if (response.text == choice) {
|
||||
return response.isCorrect;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool isCompleteByTarget(PracticeTarget target) {
|
||||
final record = _recordByTarget(target);
|
||||
if (target.activityType == ActivityTypeEnum.morphId) {
|
||||
return record.completeResponses > 0;
|
||||
}
|
||||
|
||||
return target.tokens.every(
|
||||
(t) => record.responses.any(
|
||||
(res) => res.cId == target.targetTokenConstructID(t) && res.isCorrect,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static bool isCompleteByToken(
|
||||
PracticeTarget target,
|
||||
PangeaToken token,
|
||||
) {
|
||||
final cId = target.targetTokenConstructID(token);
|
||||
return _recordByTarget(target).responses.any(
|
||||
(res) => res.cId == cId && res.isCorrect,
|
||||
);
|
||||
}
|
||||
|
||||
static bool hasAnyCorrectChoices(PracticeTarget target) {
|
||||
final record = _recordByTarget(target);
|
||||
return record.responses.any((response) => response.isCorrect);
|
||||
}
|
||||
|
||||
static bool onSelectChoice(
|
||||
String choice,
|
||||
PangeaToken token,
|
||||
PracticeActivityModel activity,
|
||||
) {
|
||||
final target = activity.practiceTarget;
|
||||
final record = _recordByTarget(target);
|
||||
final cId = target.targetTokenConstructID(token);
|
||||
if (isCompleteByTarget(target) ||
|
||||
record.alreadyHasMatchResponse(cId, choice)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final isCorrect = switch (activity) {
|
||||
MatchPracticeActivityModel() => activity.isCorrect(token, choice),
|
||||
MultipleChoicePracticeActivityModel() => activity.isCorrect(choice),
|
||||
};
|
||||
|
||||
record.addResponse(
|
||||
cId: cId,
|
||||
target: target,
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import 'dart:math';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
|
|
@ -19,6 +18,7 @@ import 'package:fluffychat/pangea/toolbar/message_practice/dotted_border_painter
|
|||
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 +48,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 +91,8 @@ class TokenPracticeButton extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
shimmer: controller.selectedMorph == null &&
|
||||
_activity?.hasAnyCorrectChoices == false,
|
||||
_activity != null &&
|
||||
!PracticeRecordController.hasAnyCorrectChoices(_activity!),
|
||||
);
|
||||
} else {
|
||||
child = _StandardMatchButton(
|
||||
|
|
@ -257,14 +255,11 @@ class _NoActivityContentButton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (practiceMode == MessagePracticeMode.wordEmoji) {
|
||||
final displayEmoji = target?.record.responses
|
||||
.firstWhereOrNull(
|
||||
(res) => res.cId == token.vocabConstructID && res.isCorrect,
|
||||
)
|
||||
?.text ??
|
||||
token.vocabConstructID.userSetEmoji ??
|
||||
'';
|
||||
if (practiceMode == MessagePracticeMode.wordEmoji && target != null) {
|
||||
final displayEmoji =
|
||||
PracticeRecordController.correctResponse(target!, token)?.text ??
|
||||
token.vocabConstructID.userSetEmoji ??
|
||||
'';
|
||||
return Text(
|
||||
displayEmoji,
|
||||
style: emojiStyle,
|
||||
|
|
|
|||
|
|
@ -1,108 +0,0 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_constants.dart';
|
||||
import 'package:fluffychat/pangea/vocab_practice/vocab_practice_session_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class VocabPracticeSessionRepo {
|
||||
static final GetStorage _storage = GetStorage('vocab_practice_session');
|
||||
|
||||
static Future<VocabPracticeSessionModel> get() async {
|
||||
final cached = _getCached();
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
final r = Random();
|
||||
final activityTypes = [
|
||||
ActivityTypeEnum.lemmaMeaning,
|
||||
//ActivityTypeEnum.lemmaAudio,
|
||||
];
|
||||
|
||||
final types = List.generate(
|
||||
VocabPracticeConstants.practiceGroupSize,
|
||||
(_) => activityTypes[r.nextInt(activityTypes.length)],
|
||||
);
|
||||
|
||||
final constructs = await _fetch();
|
||||
final targetCount = min(constructs.length, types.length);
|
||||
final targets = [
|
||||
for (var i = 0; i < targetCount; i++)
|
||||
PracticeTarget(
|
||||
tokens: [constructs[i].asToken],
|
||||
activityType: types[i],
|
||||
),
|
||||
];
|
||||
|
||||
final session = VocabPracticeSessionModel(
|
||||
userL1: MatrixState.pangeaController.userController.userL1!.langCode,
|
||||
userL2: MatrixState.pangeaController.userController.userL2!.langCode,
|
||||
startedAt: DateTime.now(),
|
||||
practiceTargets: targets,
|
||||
);
|
||||
await _setCached(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
static Future<void> update(
|
||||
VocabPracticeSessionModel session,
|
||||
) =>
|
||||
_setCached(session);
|
||||
|
||||
static Future<void> clear() => _storage.erase();
|
||||
|
||||
static Future<List<ConstructIdentifier>> _fetch() async {
|
||||
final constructs = await MatrixState
|
||||
.pangeaController.matrixState.analyticsDataService
|
||||
.getAggregatedConstructs(ConstructTypeEnum.vocab)
|
||||
.then((map) => map.values.toList());
|
||||
|
||||
// sort by last used descending, nulls first
|
||||
constructs.sort((a, b) {
|
||||
final dateA = a.lastUsed;
|
||||
final dateB = b.lastUsed;
|
||||
if (dateA == null && dateB == null) return 0;
|
||||
if (dateA == null) return -1;
|
||||
if (dateB == null) return 1;
|
||||
return dateA.compareTo(dateB);
|
||||
});
|
||||
|
||||
final Set<String> seemLemmas = {};
|
||||
final targets = <ConstructIdentifier>[];
|
||||
for (final construct in constructs) {
|
||||
if (seemLemmas.contains(construct.lemma)) continue;
|
||||
seemLemmas.add(construct.lemma);
|
||||
targets.add(construct.id);
|
||||
if (targets.length >= VocabPracticeConstants.practiceGroupSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
static VocabPracticeSessionModel? _getCached() {
|
||||
final keys = List<String>.from(_storage.getKeys());
|
||||
if (keys.isEmpty) return null;
|
||||
try {
|
||||
final json = _storage.read(keys.first) as Map<String, dynamic>;
|
||||
return VocabPracticeSessionModel.fromJson(json);
|
||||
} catch (e) {
|
||||
_storage.remove(keys.first);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _setCached(VocabPracticeSessionModel session) async {
|
||||
await _storage.erase();
|
||||
await _storage.write(
|
||||
session.startedAt.toIso8601String(),
|
||||
session.toJson(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue