add translations for error questions

and some spacing tweaks to improve layout and overflow issues
This commit is contained in:
Ava Shilling 2026-01-22 12:56:48 -05:00
parent be9ef801a9
commit 0068ef5965
5 changed files with 241 additions and 49 deletions

View file

@ -1,7 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
import 'package:fluffychat/pangea/languages/language_constants.dart';
@ -11,17 +9,20 @@ 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/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
class MorphMeaningWidget extends StatefulWidget {
final MorphFeaturesEnum feature;
final String tag;
final TextStyle? style;
final bool blankErrorFeedback;
const MorphMeaningWidget({
super.key,
required this.feature,
required this.tag,
this.style,
this.blankErrorFeedback = false,
});
@override
@ -91,12 +92,13 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
);
if (result.isError) {
return L10n.of(context).meaningNotFound;
return widget.blankErrorFeedback ? '' : L10n.of(context).meaningNotFound;
}
final morph = result.result!.getFeatureByCode(widget.feature.name);
final data = morph?.getTagByCode(widget.tag);
return data?.l1Description ?? L10n.of(context).meaningNotFound;
return data?.l1Description ??
(widget.blankErrorFeedback ? '' : L10n.of(context).meaningNotFound);
}
void _toggleEditMode(bool value) => setState(() => _editMode = value);

View file

@ -505,4 +505,5 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
@override
Widget build(BuildContext context) => AnalyticsPracticeView(this);
final request = activityTarget.value;
}

View file

@ -267,6 +267,7 @@ class AnalyticsPracticeSessionRepo {
choreo: choreo,
stepIndex: i,
eventID: event.eventId,
event: event,
),
),
);

View file

@ -1,4 +1,5 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.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';
@ -74,8 +75,7 @@ class AnalyticsPracticeView extends StatelessWidget {
),
body: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 24.0,
horizontal: 8.0,
),
child: MaxWidthBody(
withScrolling: false,
@ -123,25 +123,36 @@ class _AnalyticsActivityView extends StatelessWidget {
),
Expanded(
child: Column(
spacing: 16.0,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
flex: 1,
child: ValueListenableBuilder(
valueListenable: controller.activityTarget,
builder: (context, target, __) => target != null
? Column(
ValueListenableBuilder(
valueListenable: controller.activityTarget,
builder: (context, target, __) => target != null
? Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 16.0,
),
child: Column(
spacing: 12.0,
children: [
Text(
target.promptText(context),
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.titleLarge
?.copyWith(
fontWeight: FontWeight.bold,
),
style: FluffyThemes.isColumnMode(context)
? Theme.of(context)
.textTheme
.titleLarge
?.copyWith(
fontWeight: FontWeight.bold,
)
: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(
fontWeight: FontWeight.bold,
),
),
if (controller.widget.type ==
ConstructTypeEnum.vocab)
@ -153,22 +164,56 @@ class _AnalyticsActivityView extends StatelessWidget {
style: const TextStyle(fontSize: 14.0),
),
],
)
: const SizedBox(),
),
),
)
: const SizedBox.shrink(),
),
Expanded(
flex: 2,
child: Center(
Flexible(
fit: FlexFit.loose,
child: SingleChildScrollView(
child: _AnalyticsPracticeCenterContent(
controller: controller,
),
),
),
Expanded(
flex: 6,
child: _ActivityChoicesWidget(controller),
),
//reserve space for grammar category morph meaning to avoid shifting, but only in those questions
AnimatedBuilder(
animation: Listenable.merge([
controller.activityState,
controller.selectedMorphChoice,
]),
builder: (context, _) {
final activityState = controller.activityState.value;
final selectedChoice = controller.selectedMorphChoice.value;
final isGrammarCategory = activityState
is AsyncLoaded<MultipleChoicePracticeActivityModel> &&
activityState.value.activityType ==
ActivityTypeEnum.grammarCategory;
if (!isGrammarCategory) {
return const SizedBox.shrink();
}
return ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 80,
),
child: selectedChoice == null
? const SizedBox.shrink()
: SingleChildScrollView(
child: MorphMeaningWidget(
feature: selectedChoice.feature,
tag: selectedChoice.tag,
blankErrorFeedback: true,
),
),
);
},
),
],
),
),
@ -193,8 +238,23 @@ class _AnalyticsPracticeCenterContent extends StatelessWidget {
ActivityTypeEnum.grammarError => ValueListenableBuilder(
valueListenable: controller.activityState,
builder: (context, state, __) => switch (state) {
AsyncLoaded(value: final activity) => _ErrorBlankWidget(
activity: activity as GrammarErrorPracticeActivityModel,
AsyncLoaded(
value: final GrammarErrorPracticeActivityModel activity
) =>
Column(
mainAxisSize: MainAxisSize.min,
children: [
_ErrorBlankWidget(
activity: activity,
),
const SizedBox(height: 12),
_GrammarErrorTranslationButton(
key: ValueKey(
'${activity.eventID}_${activity.errorOffset}_${activity.errorLength}',
),
controller: controller,
),
],
),
_ => const SizedBox(),
},
@ -349,11 +409,10 @@ class _ActivityChoicesWidget extends StatelessWidget {
return Column(
children: [
Container(
constraints: const BoxConstraints(maxHeight: 400.0),
Expanded(
child: Column(
spacing: 4.0,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: choices
.map(
(choice) => _ChoiceCard(
@ -372,20 +431,6 @@ class _ActivityChoicesWidget extends StatelessWidget {
.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,
);
},
),
],
);
},
@ -470,6 +515,7 @@ class _ChoiceCard extends StatelessWidget {
tag: choiceText,
onPressed: onPressed,
isCorrect: isCorrect,
height: cardHeight,
enabled: enabled,
);
@ -504,3 +550,144 @@ class _ChoiceCard extends StatelessWidget {
}
}
}
class _GrammarErrorTranslationButton extends StatefulWidget {
final AnalyticsPracticeState controller;
const _GrammarErrorTranslationButton({
super.key,
required this.controller,
});
@override
State<_GrammarErrorTranslationButton> createState() =>
_GrammarErrorTranslationButtonState();
}
class _GrammarErrorTranslationButtonState
extends State<_GrammarErrorTranslationButton> {
Future<String>? _translationFuture;
bool _showTranslation = false;
void _toggleTranslation() {
if (_showTranslation) {
setState(() {
_showTranslation = false;
_translationFuture = null;
});
} else {
setState(() {
_showTranslation = true;
_translationFuture = widget.controller.requestTranslation();
});
}
}
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: _toggleTranslation,
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8.0,
children: [
if (_showTranslation)
Flexible(
child: FutureBuilder<String>(
future: _translationFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
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: const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
),
);
}
if (snapshot.hasError) {
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: Text(
L10n.of(context).oopsSomethingWentWrong,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryFixed,
fontSize: AppConfig.fontSizeFactor *
AppConfig.messageFontSize,
),
),
);
}
if (snapshot.hasData) {
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: Text(
snapshot.data!,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryFixed,
fontSize: AppConfig.fontSizeFactor *
AppConfig.messageFontSize,
),
textAlign: TextAlign.center,
),
);
}
return const SizedBox();
},
),
),
if (!_showTranslation)
ElevatedButton(
onPressed: _toggleTranslation,
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
padding: const EdgeInsets.all(8),
),
child: const Icon(
Icons.lightbulb_outline,
size: 20,
),
),
],
),
),
);
}
}

View file

@ -1,13 +1,12 @@
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/choreographer/choreo_record_model.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.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';
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
// includes feedback text and the bad activity model
class ActivityQualityFeedback {
@ -45,11 +44,13 @@ class GrammarErrorRequestInfo {
final ChoreoRecordModel choreo;
final int stepIndex;
final String eventID;
final PangeaMessageEvent? event;
const GrammarErrorRequestInfo({
required this.choreo,
required this.stepIndex,
required this.eventID,
this.event,
});
Map<String, dynamic> toJson() {