diff --git a/lib/pangea/analytics_data/analytics_data_service.dart b/lib/pangea/analytics_data/analytics_data_service.dart index 38ecd863c..bd371aa81 100644 --- a/lib/pangea/analytics_data/analytics_data_service.dart +++ b/lib/pangea/analytics_data/analytics_data_service.dart @@ -242,6 +242,7 @@ class AnalyticsDataService { String? roomId, DateTime? since, ConstructUseTypeEnum? type, + bool filterCapped = true, }) async { await _ensureInitialized(); final uses = await _analyticsClientGetter.database.getUses( @@ -264,7 +265,8 @@ class AnalyticsDataService { cappedLastUseCache[use.identifier] = constructs.cappedLastUse; } final cappedLastUse = cappedLastUseCache[use.identifier]; - if (cappedLastUse != null && use.timeStamp.isAfter(cappedLastUse)) { + if (filterCapped && + (cappedLastUse != null && use.timeStamp.isAfter(cappedLastUse))) { continue; } filtered.add(use); diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index 44b471f92..1bcc52d01 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -23,7 +23,6 @@ import 'package:fluffychat/pangea/morphs/morph_features_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_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/toolbar/message_practice/practice_record_controller.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -101,6 +100,8 @@ class AnalyticsPracticeState extends State final ValueNotifier selectedMorphChoice = ValueNotifier(null); + final ValueNotifier hintPressedNotifier = ValueNotifier(false); + final Map> _choiceTexts = {}; final Map> _choiceEmojis = {}; @@ -125,6 +126,7 @@ class AnalyticsPracticeState extends State progressNotifier.dispose(); enableChoicesNotifier.dispose(); selectedMorphChoice.dispose(); + hintPressedNotifier.dispose(); super.dispose(); } @@ -210,6 +212,7 @@ class AnalyticsPracticeState extends State activityState.value = const AsyncState.loading(); activityTarget.value = null; selectedMorphChoice.value = null; + hintPressedNotifier.value = false; enableChoicesNotifier.value = true; progressNotifier.value = 0.0; _queue.clear(); @@ -282,6 +285,7 @@ class AnalyticsPracticeState extends State try { activityState.value = const AsyncState.loading(); selectedMorphChoice.value = null; + hintPressedNotifier.value = false; final req = activityTarget.value!; final res = await _fetchActivity(req); @@ -324,6 +328,7 @@ class AnalyticsPracticeState extends State while (_queue.isNotEmpty) { activityState.value = const AsyncState.loading(); selectedMorphChoice.value = null; + hintPressedNotifier.value = false; final nextActivityCompleter = _queue.removeFirst(); try { @@ -477,6 +482,10 @@ class AnalyticsPracticeState extends State await _analyticsService.updateService.addAnalytics(null, [use]); } + void onHintPressed() { + hintPressedNotifier.value = !hintPressedNotifier.value; + } + Future onSelectChoice( String choiceContent, ) async { @@ -528,21 +537,19 @@ class AnalyticsPracticeState extends State } Future?> getExampleMessage( - PracticeTarget target, + MessageActivityRequest activityRequest, ) async { + final target = activityRequest.target; final token = target.tokens.first; final construct = target.targetTokenConstructID(token); - String? form; if (widget.type == ConstructTypeEnum.morph) { - if (target.morphFeature == null) return null; - form = token.lemma.form; + return activityRequest.morphExampleInfo?.exampleMessage; } return ExampleMessageUtil.getExampleMessage( await _analyticsService.getConstructUse(construct), Matrix.of(context).client, - form: form, ); } diff --git a/lib/pangea/analytics_practice/analytics_practice_session_model.dart b/lib/pangea/analytics_practice/analytics_practice_session_model.dart index 9ab395a13..ebeccdce0 100644 --- a/lib/pangea/analytics_practice/analytics_practice_session_model.dart +++ b/lib/pangea/analytics_practice/analytics_practice_session_model.dart @@ -1,21 +1,70 @@ +import 'package:flutter/painting.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_practice/analytics_practice_constants.dart'; import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; +class MorphExampleInfo { + final List exampleMessage; + + const MorphExampleInfo({ + required this.exampleMessage, + }); + + Map toJson() { + final segments = >[]; + + for (final span in exampleMessage) { + if (span is TextSpan) { + segments.add({ + 'text': span.text ?? '', + 'isBold': span.style?.fontWeight == FontWeight.bold, + }); + } + } + + return { + 'segments': segments, + }; + } + + factory MorphExampleInfo.fromJson(Map json) { + final segments = json['segments'] as List? ?? []; + + final spans = []; + for (final segment in segments) { + final text = segment['text'] as String? ?? ''; + final isBold = segment['isBold'] as bool? ?? false; + + spans.add( + TextSpan( + text: text, + style: isBold ? const TextStyle(fontWeight: FontWeight.bold) : null, + ), + ); + } + + return MorphExampleInfo(exampleMessage: spans); + } +} + class AnalyticsActivityTarget { final PracticeTarget target; final GrammarErrorRequestInfo? grammarErrorInfo; + final MorphExampleInfo? morphExampleInfo; AnalyticsActivityTarget({ required this.target, this.grammarErrorInfo, + this.morphExampleInfo, }); Map toJson() => { 'target': target.toJson(), 'grammarErrorInfo': grammarErrorInfo?.toJson(), + 'morphExampleInfo': morphExampleInfo?.toJson(), }; factory AnalyticsActivityTarget.fromJson(Map json) => @@ -24,6 +73,9 @@ class AnalyticsActivityTarget { grammarErrorInfo: json['grammarErrorInfo'] != null ? GrammarErrorRequestInfo.fromJson(json['grammarErrorInfo']) : null, + morphExampleInfo: json['morphExampleInfo'] != null + ? MorphExampleInfo.fromJson(json['morphExampleInfo']) + : null, ); } @@ -79,6 +131,7 @@ class AnalyticsPracticeSessionModel { activityQualityFeedback: null, target: target.target, grammarErrorInfo: target.grammarErrorInfo, + morphExampleInfo: target.morphExampleInfo, ); }).toList(); } diff --git a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart index c2c507741..5e82d6b6d 100644 --- a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart +++ b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/example_message_util.dart'; import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constants.dart'; import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart'; import 'package:fluffychat/pangea/common/network/requests.dart'; @@ -12,8 +13,11 @@ import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; +import 'package:fluffychat/pangea/languages/language_constants.dart'; import 'package:fluffychat/pangea/lemmas/lemma.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; +import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart'; +import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_request.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; @@ -63,15 +67,18 @@ class AnalyticsPracticeSessionRepo { final remainingCount = (AnalyticsPracticeConstants.practiceGroupSize + AnalyticsPracticeConstants.errorBufferSize) - targets.length; - final morphEntries = morphs.entries.take(remainingCount); + final morphEntries = morphs.take(remainingCount); for (final entry in morphEntries) { targets.add( AnalyticsActivityTarget( target: PracticeTarget( - tokens: [entry.key], + tokens: [entry.token], activityType: ActivityTypeEnum.grammarCategory, - morphFeature: entry.value, + morphFeature: entry.feature, + ), + morphExampleInfo: MorphExampleInfo( + exampleMessage: entry.exampleMessage, ), ), ); @@ -125,12 +132,37 @@ class AnalyticsPracticeSessionRepo { return targets; } - static Future> _fetchMorphs() async { + static Future> _fetchMorphs() async { final constructs = await MatrixState .pangeaController.matrixState.analyticsDataService .getAggregatedConstructs(ConstructTypeEnum.morph) .then((map) => map.values.toList()); + final morphInfoRequest = MorphInfoRequest( + userL1: MatrixState.pangeaController.userController.userL1?.langCode ?? + LanguageKeys.defaultLanguage, + userL2: MatrixState.pangeaController.userController.userL2?.langCode ?? + LanguageKeys.defaultLanguage, + ); + + final morphInfoResult = await MorphInfoRepo.get( + MatrixState.pangeaController.userController.accessToken, + morphInfoRequest, + ); + + // Build list of features with multiple tags (valid for practice) + final List validFeatures = []; + if (!morphInfoResult.isError) { + final response = morphInfoResult.asValue?.value; + if (response != null) { + for (final feature in response.features) { + if (feature.tags.length > 1) { + validFeatures.add(feature.code); + } + } + } + } + // sort by last used descending, nulls first constructs.sort((a, b) { final dateA = a.lastUsed; @@ -141,7 +173,7 @@ class AnalyticsPracticeSessionRepo { return dateA.compareTo(dateB); }); - final targets = {}; + final targets = []; final Set seenForms = {}; for (final entry in constructs) { @@ -152,10 +184,14 @@ class AnalyticsPracticeSessionRepo { } final feature = MorphFeaturesEnumExtension.fromString(entry.id.category); - if (feature == MorphFeaturesEnum.Unknown) { + + // Only include features that are in the valid list (have multiple tags) + if (feature == MorphFeaturesEnum.Unknown || + (validFeatures.isNotEmpty && !validFeatures.contains(feature.name))) { continue; } + List? exampleMessage; for (final use in entry.cappedUses) { if (targets.length >= (AnalyticsPracticeConstants.practiceGroupSize + @@ -169,6 +205,17 @@ class AnalyticsPracticeSessionRepo { continue; } + exampleMessage = await ExampleMessageUtil.getExampleMessage( + await MatrixState.pangeaController.matrixState.analyticsDataService + .getConstructUse(entry.id), + MatrixState.pangeaController.matrixState.client, + form: form, + ); + + if (exampleMessage == null) { + continue; + } + seenForms.add(form); final token = PangeaToken( lemma: Lemma( @@ -180,7 +227,13 @@ class AnalyticsPracticeSessionRepo { pos: 'other', morph: {feature: use.lemma}, ); - targets[token] = feature; + targets.add( + MorphPracticeTarget( + feature: feature, + token: token, + exampleMessage: exampleMessage, + ), + ); break; } } @@ -189,14 +242,32 @@ class AnalyticsPracticeSessionRepo { } static Future> _fetchErrors() async { - final uses = await MatrixState + // Fetch all recent uses in one call (not filtering blocked constructs) + final allRecentUses = await MatrixState .pangeaController.matrixState.analyticsDataService - .getUses(count: 100, type: ConstructUseTypeEnum.ga); + .getUses(count: 200, filterCapped: false); + + // Filter for grammar error uses + final grammarErrorUses = allRecentUses + .where((use) => use.useType == ConstructUseTypeEnum.ga) + .toList(); + + // Create list of recently used constructs + final cutoffTime = DateTime.now().subtract(const Duration(hours: 24)); + final recentlyPracticedConstructs = allRecentUses + .where( + (use) => + use.metadata.timeStamp.isAfter(cutoffTime) && + (use.useType == ConstructUseTypeEnum.corGE || + use.useType == ConstructUseTypeEnum.incGE), + ) + .map((use) => use.identifier) + .toSet(); final client = MatrixState.pangeaController.matrixState.client; final Map idsToEvents = {}; - for (final use in uses) { + for (final use in grammarErrorUses) { final eventID = use.metadata.eventId; if (eventID == null || idsToEvents.containsKey(eventID)) continue; @@ -257,7 +328,11 @@ class AnalyticsPracticeSessionRepo { if (igcMatch!.match.offset == 0 && igcMatch.match.length >= stepText.trim().characters.length) { - // Skip if the grammar error spans the entire step + continue; + } + + if (igcMatch.match.isNormalizationError()) { + // Skip normalization errors continue; } @@ -272,8 +347,22 @@ class AnalyticsPracticeSessionRepo { ) .toList(); - // Skip if no valid tokens found for this grammar error - if (choiceTokens.isEmpty) continue; + // Skip if no valid tokens found for this grammar error, or only one answer + if (choiceTokens.length <= 1) { + continue; + } + + final firstToken = choiceTokens.first; + final tokenIdentifier = ConstructIdentifier( + lemma: firstToken.lemma.text, + type: ConstructTypeEnum.vocab, + category: firstToken.pos, + ); + + final hasRecentPractice = + recentlyPracticedConstructs.contains(tokenIdentifier); + + if (hasRecentPractice) continue; String? translation; try { @@ -291,6 +380,7 @@ class AnalyticsPracticeSessionRepo { } if (translation == null) continue; + targets.add( AnalyticsActivityTarget( target: PracticeTarget( @@ -312,3 +402,15 @@ class AnalyticsPracticeSessionRepo { return targets; } } + +class MorphPracticeTarget { + final PangeaToken token; + final MorphFeaturesEnum feature; + final List exampleMessage; + + MorphPracticeTarget({ + required this.token, + required this.feature, + required this.exampleMessage, + }); +} diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index d1212af7c..9ada4f9d2 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -16,6 +16,7 @@ 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'; +import 'package:fluffychat/pangea/common/widgets/pressable_button.dart'; import 'package:fluffychat/pangea/instructions/instructions_enum.dart'; import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'; import 'package:fluffychat/pangea/phonetic_transcription/phonetic_transcription_widget.dart'; @@ -161,28 +162,7 @@ class _AnalyticsActivityView extends StatelessWidget { const SizedBox(height: 16.0), _ActivityChoicesWidget(controller), const SizedBox(height: 16.0), - ListenableBuilder( - listenable: Listenable.merge([ - controller.activityState, - controller.selectedMorphChoice, - ]), - builder: (context, _) { - final activityState = controller.activityState.value; - final selectedChoice = controller.selectedMorphChoice.value; - - if (activityState - is! AsyncLoaded || - selectedChoice == null) { - return const SizedBox.shrink(); - } - - return MorphMeaningWidget( - feature: selectedChoice.feature, - tag: selectedChoice.tag, - blankErrorFeedback: true, - ); - }, - ), + _WrongAnswerFeedback(controller: controller), ], ); } @@ -214,15 +194,12 @@ class _AnalyticsPracticeCenterContent extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ _ErrorBlankWidget( - activity: activity, - ), - const SizedBox(height: 12), - _GrammarErrorTranslationButton( key: ValueKey( '${activity.eventID}_${activity.errorOffset}_${activity.errorLength}', ), - translation: activity.translation, + activity: activity, ), + const SizedBox(height: 12), ], ), _ => const SizedBox(), @@ -230,11 +207,31 @@ class _AnalyticsPracticeCenterContent extends StatelessWidget { ), ), ), + ActivityTypeEnum.grammarCategory => Center( + child: Column( + children: [ + _CorrectAnswerHint(controller: controller), + _ExampleMessageWidget( + controller.getExampleMessage(target!), + ), + const SizedBox(height: 12), + ValueListenableBuilder( + valueListenable: controller.hintPressedNotifier, + builder: (context, hintPressed, __) { + return HintButton( + depressed: hintPressed, + onPressed: controller.onHintPressed, + ); + }, + ), + ], + ), + ), _ => SizedBox( height: 100.0, child: Center( child: _ExampleMessageWidget( - controller.getExampleMessage(target!.target), + controller.getExampleMessage(target!), ), ), ), @@ -284,18 +281,123 @@ class _ExampleMessageWidget extends StatelessWidget { } } -class _ErrorBlankWidget extends StatelessWidget { - final GrammarErrorPracticeActivityModel activity; +class _CorrectAnswerHint extends StatelessWidget { + final AnalyticsPracticeState controller; - const _ErrorBlankWidget({ - required this.activity, + const _CorrectAnswerHint({ + required this.controller, }); @override Widget build(BuildContext context) { - final text = activity.text; - final errorOffset = activity.errorOffset; - final errorLength = activity.errorLength; + return ValueListenableBuilder( + valueListenable: controller.hintPressedNotifier, + builder: (context, hintPressed, __) { + if (!hintPressed) { + return const SizedBox.shrink(); + } + + return ValueListenableBuilder( + valueListenable: controller.activityState, + builder: (context, state, __) { + if (state is! AsyncLoaded) { + return const SizedBox.shrink(); + } + + final activity = state.value; + if (activity is! MorphPracticeActivityModel) { + return const SizedBox.shrink(); + } + + final correctAnswerTag = + activity.multipleChoiceContent.answers.first; + + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: MorphMeaningWidget( + feature: activity.morphFeature, + tag: correctAnswerTag, + ), + ); + }, + ); + }, + ); + } +} + +class _WrongAnswerFeedback extends StatelessWidget { + final AnalyticsPracticeState controller; + + const _WrongAnswerFeedback({ + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: Listenable.merge([ + controller.activityState, + controller.selectedMorphChoice, + ]), + builder: (context, _) { + final activityState = controller.activityState.value; + final selectedChoice = controller.selectedMorphChoice.value; + + if (activityState + is! AsyncLoaded || + selectedChoice == null) { + return const SizedBox.shrink(); + } + + final activity = activityState.value; + final isWrongAnswer = + !activity.multipleChoiceContent.isCorrect(selectedChoice.tag); + + if (!isWrongAnswer) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: MorphMeaningWidget( + feature: selectedChoice.feature, + tag: selectedChoice.tag, + blankErrorFeedback: true, + ), + ); + }, + ); + } +} + +class _ErrorBlankWidget extends StatefulWidget { + final GrammarErrorPracticeActivityModel activity; + + const _ErrorBlankWidget({ + super.key, + required this.activity, + }); + + @override + State<_ErrorBlankWidget> createState() => _ErrorBlankWidgetState(); +} + +class _ErrorBlankWidgetState extends State<_ErrorBlankWidget> { + late final String translation = widget.activity.translation; + bool _showTranslation = false; + + void _toggleTranslation() { + setState(() { + _showTranslation = !_showTranslation; + }); + } + + @override + Widget build(BuildContext context) { + final text = widget.activity.text; + final errorOffset = widget.activity.errorOffset; + final errorLength = widget.activity.errorLength; const maxContextChars = 50; @@ -342,122 +444,107 @@ class _ErrorBlankWidget extends StatelessWidget { final after = chars.skip(errorEnd).take(afterEnd - errorEnd).toString(); - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - color: Color.alphaBlend( - Colors.white.withAlpha(180), - ThemeData.dark().colorScheme.primary, - ), - borderRadius: BorderRadius.circular(16), - ), - child: RichText( - text: TextSpan( - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryFixed, - fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, + return Column( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, ), - children: [ - if (trimmedBefore) const TextSpan(text: '…'), - if (before.isNotEmpty) TextSpan(text: before), - WidgetSpan( - child: Container( - height: 4.0, - width: (errorLength * 8).toDouble(), - padding: const EdgeInsets.only(bottom: 2.0), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, + decoration: BoxDecoration( + color: Color.alphaBlend( + Colors.white.withAlpha(180), + ThemeData.dark().colorScheme.primary, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + RichText( + text: TextSpan( + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryFixed, + fontSize: + AppConfig.fontSizeFactor * AppConfig.messageFontSize, + ), + children: [ + if (trimmedBefore) const TextSpan(text: '…'), + if (before.isNotEmpty) TextSpan(text: before), + WidgetSpan( + child: Container( + height: 4.0, + width: (errorLength * 8).toDouble(), + padding: const EdgeInsets.only(bottom: 2.0), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + if (after.isNotEmpty) TextSpan(text: after), + if (trimmedAfter) const TextSpan(text: '…'), + ], ), ), - ), - if (after.isNotEmpty) TextSpan(text: after), - if (trimmedAfter) const TextSpan(text: '…'), - ], + const SizedBox(height: 8), + _showTranslation + ? Text( + translation, + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryFixed, + fontSize: AppConfig.fontSizeFactor * + AppConfig.messageFontSize, + fontStyle: FontStyle.italic, + ), + textAlign: TextAlign.left, + ) + : const SizedBox.shrink(), + ], + ), ), - ), + const SizedBox(height: 8), + HintButton(depressed: _showTranslation, onPressed: _toggleTranslation), + ], ); } } -class _GrammarErrorTranslationButton extends StatefulWidget { - final String translation; +class HintButton extends StatelessWidget { + final VoidCallback onPressed; + final bool depressed; - const _GrammarErrorTranslationButton({ + const HintButton({ + required this.onPressed, + required this.depressed, super.key, - required this.translation, }); - @override - State<_GrammarErrorTranslationButton> createState() => - _GrammarErrorTranslationButtonState(); -} - -class _GrammarErrorTranslationButtonState - extends State<_GrammarErrorTranslationButton> { - bool _showTranslation = false; - - void _toggleTranslation() { - if (_showTranslation) { - setState(() { - _showTranslation = false; - }); - } else { - setState(() { - _showTranslation = true; - }); - } - } - @override Widget build(BuildContext context) { - return Center( - child: GestureDetector( - onTap: _toggleTranslation, - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 8.0, - children: [ - if (_showTranslation) - Flexible( - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - color: Color.alphaBlend( - Colors.white.withAlpha(180), - ThemeData.dark().colorScheme.primary, - ), - borderRadius: BorderRadius.circular(16), - ), - child: Text( - widget.translation, - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryFixed, - fontSize: - AppConfig.fontSizeFactor * AppConfig.messageFontSize, - ), - ), - ), - ) - else - ElevatedButton( - onPressed: _toggleTranslation, - style: ElevatedButton.styleFrom( - shape: const CircleBorder(), - padding: const EdgeInsets.all(8), - ), - child: const Icon( - Icons.lightbulb_outline, - size: 20, - ), - ), - ], - ), + return PressableButton( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.primaryContainer, + onPressed: onPressed, + depressed: depressed, + playSound: true, + colorFactor: 0.3, + builder: (context, depressed, shadowColor) => Stack( + alignment: Alignment.center, + children: [ + Container( + height: 40.0, + width: 40.0, + decoration: BoxDecoration( + color: depressed + ? shadowColor + : Theme.of(context).colorScheme.primaryContainer, + shape: BoxShape.circle, + ), + ), + const Icon( + Icons.lightbulb_outline, + size: 20, + ), + ], ), ); } diff --git a/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart b/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart index 7ff543247..230944160 100644 --- a/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart +++ b/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart @@ -35,7 +35,7 @@ class GrammarChoiceCard extends StatelessWidget { final baseTextSize = (Theme.of(context).textTheme.titleMedium?.fontSize ?? 16) * (height / 72.0).clamp(1.0, 1.4); - final emojiSize = baseTextSize * 1.2; + final emojiSize = baseTextSize * 1.5; final copy = getGrammarCopy( category: feature.name, lemma: tag, @@ -54,7 +54,7 @@ class GrammarChoiceCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( - width: height * .7, + width: height, height: height, child: Center( child: MorphIcon( diff --git a/lib/pangea/analytics_practice/morph_category_activity_generator.dart b/lib/pangea/analytics_practice/morph_category_activity_generator.dart index 545e466ba..78ef8ba78 100644 --- a/lib/pangea/analytics_practice/morph_category_activity_generator.dart +++ b/lib/pangea/analytics_practice/morph_category_activity_generator.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart'; 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'; @@ -58,6 +59,8 @@ class MorphCategoryActivityGenerator { choices: choices.toSet(), answers: {morphTag}, ), + morphExampleInfo: + req.morphExampleInfo ?? const MorphExampleInfo(exampleMessage: []), ), ); } diff --git a/lib/pangea/practice_activities/message_activity_request.dart b/lib/pangea/practice_activities/message_activity_request.dart index 7452dd026..ec6a9e491 100644 --- a/lib/pangea/practice_activities/message_activity_request.dart +++ b/lib/pangea/practice_activities/message_activity_request.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart'; import 'package:fluffychat/pangea/choreographer/choreo_record_model.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; @@ -79,6 +80,7 @@ class MessageActivityRequest { final PracticeTarget target; final ActivityQualityFeedback? activityQualityFeedback; final GrammarErrorRequestInfo? grammarErrorInfo; + final MorphExampleInfo? morphExampleInfo; MessageActivityRequest({ required this.userL1, @@ -86,6 +88,7 @@ class MessageActivityRequest { required this.activityQualityFeedback, required this.target, this.grammarErrorInfo, + this.morphExampleInfo, }) { if (target.tokens.isEmpty) { throw Exception('Target tokens must not be empty'); diff --git a/lib/pangea/practice_activities/practice_activity_model.dart b/lib/pangea/practice_activities/practice_activity_model.dart index c0531dcba..eddbdacf1 100644 --- a/lib/pangea/practice_activities/practice_activity_model.dart +++ b/lib/pangea/practice_activities/practice_activity_model.dart @@ -3,6 +3,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; +import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_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'; @@ -111,6 +112,9 @@ sealed class PracticeActivityModel { tokens: tokens, morphFeature: morph!, multipleChoiceContent: multipleChoiceContent!, + morphExampleInfo: json['morph_example_info'] != null + ? MorphExampleInfo.fromJson(json['morph_example_info']) + : const MorphExampleInfo(exampleMessage: []), ); case ActivityTypeEnum.lemmaAudio: assert( @@ -302,11 +306,13 @@ sealed class MorphPracticeActivityModel } class MorphCategoryPracticeActivityModel extends MorphPracticeActivityModel { + final MorphExampleInfo morphExampleInfo; MorphCategoryPracticeActivityModel({ required super.tokens, required super.langCode, required super.morphFeature, required super.multipleChoiceContent, + required this.morphExampleInfo, }); @override @@ -330,6 +336,13 @@ class MorphCategoryPracticeActivityModel extends MorphPracticeActivityModel { xp: useType.pointValue, ); } + + @override + Map toJson() { + final json = super.toJson(); + json['morph_example_info'] = morphExampleInfo.toJson(); + return json; + } } class MorphMatchPracticeActivityModel extends MorphPracticeActivityModel {