grammar practice UI updates

- add morph icon to card
- track last selected answer and display hint/description at the bottom after each one (like chat practice)
This commit is contained in:
Ava Shilling 2026-01-16 16:35:26 -05:00
parent af26fd3bd9
commit 78ca8832cd
3 changed files with 98 additions and 23 deletions

View file

@ -17,6 +17,7 @@ import 'package:fluffychat/pangea/analytics_practice/analytics_practice_view.dar
import 'package:fluffychat/pangea/common/utils/async_state.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart';
import 'package:fluffychat/pangea/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';
@ -26,6 +27,16 @@ import 'package:fluffychat/pangea/toolbar/message_practice/practice_record_contr
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class SelectedMorphChoice {
final MorphFeaturesEnum feature;
final String tag;
const SelectedMorphChoice({
required this.feature,
required this.tag,
});
}
class PracticeChoice {
final String choiceId;
final String choiceText;
@ -74,6 +85,9 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
final ValueNotifier<double> progressNotifier = ValueNotifier<double>(0.0);
final ValueNotifier<SelectedMorphChoice?> selectedMorphChoice =
ValueNotifier<SelectedMorphChoice?>(null);
final Map<String, Map<String, String>> _choiceTexts = {};
final Map<String, Map<String, String?>> _choiceEmojis = {};
@ -101,6 +115,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
activityState.dispose();
activityTarget.dispose();
progressNotifier.dispose();
selectedMorphChoice.dispose();
super.dispose();
}
@ -185,6 +200,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
void _resetActivityState() {
activityState.value = const AsyncState.loading();
activityTarget.value = null;
selectedMorphChoice.value = null;
}
void _resetSessionState() {
@ -285,6 +301,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
await _completeSession();
} else {
activityState.value = const AsyncState.loading();
selectedMorphChoice.value = null;
final nextActivityCompleter = _queue.removeFirst();
activityTarget.value = nextActivityCompleter.key;
@ -401,6 +418,14 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
if (_currentActivity == null) return;
final activity = _currentActivity!;
// Track the selection for display
if (activity is MorphPracticeActivityModel) {
selectedMorphChoice.value = SelectedMorphChoice(
feature: activity.morphFeature,
tag: choiceContent,
);
}
// Update activity record
PracticeRecordController.onSelectChoice(
choiceContent,

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_details_popup/morph_meaning_widget.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';
@ -263,28 +264,46 @@ class _ActivityChoicesWidget extends StatelessWidget {
final cardHeight = (constrainedHeight / (choices.length + 1))
.clamp(50.0, 80.0);
return Container(
constraints: const BoxConstraints(maxHeight: 400.0),
child: Column(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: choices
.map(
(choice) => _ChoiceCard(
activity: value,
targetId:
controller.choiceTargetId(choice.choiceId),
choiceId: choice.choiceId,
onPressed: () => controller.onSelectChoice(
choice.choiceId,
),
cardHeight: cardHeight,
choiceText: choice.choiceText,
choiceEmoji: choice.choiceEmoji,
),
)
.toList(),
),
return Column(
children: [
Container(
constraints: const BoxConstraints(maxHeight: 400.0),
child: Column(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
children: choices
.map(
(choice) => _ChoiceCard(
activity: value,
targetId:
controller.choiceTargetId(choice.choiceId),
choiceId: choice.choiceId,
onPressed: () => controller.onSelectChoice(
choice.choiceId,
),
cardHeight: cardHeight,
choiceText: choice.choiceText,
choiceEmoji: choice.choiceEmoji,
),
)
.toList(),
),
),
if (value.activityType == ActivityTypeEnum.grammarCategory)
ValueListenableBuilder(
valueListenable: controller.selectedMorphChoice,
builder: (context, selectedChoice, __) {
if (selectedChoice == null) {
return const SizedBox.shrink();
}
return MorphMeaningWidget(
feature: selectedChoice.feature,
tag: selectedChoice.tag,
);
},
),
],
);
},
),

View file

@ -3,6 +3,7 @@ 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';
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
/// Choice card for meaning activity with emoji, and alt text on flip
class GrammarChoiceCard extends StatelessWidget {
@ -29,6 +30,10 @@ class GrammarChoiceCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final baseTextSize =
(Theme.of(context).textTheme.titleMedium?.fontSize ?? 16) *
(height / 72.0).clamp(1.0, 1.4);
final emojiSize = baseTextSize * 1.2;
final copy = getGrammarCopy(
category: feature.name,
lemma: tag,
@ -42,7 +47,33 @@ class GrammarChoiceCard extends StatelessWidget {
onPressed: onPressed,
isCorrect: isCorrect,
height: height,
child: Text(copy),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: height * .7,
height: height,
child: Center(
child: MorphIcon(
morphFeature: feature,
morphTag: tag,
size: Size(emojiSize, emojiSize),
),
),
),
Expanded(
child: Text(
copy,
overflow: TextOverflow.ellipsis,
maxLines: 2,
textAlign: TextAlign.left,
style: TextStyle(
fontSize: baseTextSize,
),
),
),
],
),
);
}
}