chore(reading_assistance): more tweaks based on feedback

This commit is contained in:
wcjord 2025-03-25 17:44:36 -04:00
parent 42f56c1c54
commit ac5356acd7
30 changed files with 692 additions and 587 deletions

View file

@ -4814,7 +4814,7 @@
"pleaseEnterInt": "Please enter a number",
"home": "Home",
"join": "Join",
"readingAssistanceOverviewBody": "Click the buttons below for mini-games on visualizing vocab, practice listening, meaning, and grammar concepts. Click any word for details.",
"readingAssistanceOverviewBody": "Click the buttons below for mini-games on matching emojis, audios, word meanings, and grammar concepts. Or click on any word for details.",
"learnByTexting": "Learn by texting",
"levelSummaryTrigger": "View summary",
"levelSummaryPopupTitle": "Level {level} Summary",

View file

@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
abstract class AppConfig {
// #Pangea
@ -28,7 +26,7 @@ abstract class AppConfig {
static const double toolbarMinWidth = 350.0;
static const double defaultHeaderHeight = 56.0;
static const double readingAssistanceInputBarHeight = 170;
static const double toolbarButtonsHeight = 100.0;
static const double toolbarButtonsHeight = 50.0;
static const double toolbarSpacing = 8.0;
static const double toolbarIconSize = 24.0;

View file

@ -4,22 +4,9 @@ import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:collection/collection.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:matrix/matrix.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:universal_html/html.dart' as html;
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/config/themes.dart';
@ -64,6 +51,18 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/share_scaffold_dialog.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:matrix/matrix.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:universal_html/html.dart' as html;
import '../../utils/account_bundles.dart';
import '../../utils/localized_exception_extension.dart';
import 'send_file_dialog.dart';

View file

@ -1,7 +1,4 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
@ -15,6 +12,7 @@ import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/morphs/morph_icon.dart';
import 'package:fluffychat/pangea/user/client_extension.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
class MorphAnalyticsListView extends StatelessWidget {
final void Function(ConstructIdentifier) onConstructZoom;
@ -88,9 +86,7 @@ class MorphFeatureBox extends StatelessWidget {
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
border: Border.all(
color: Theme.of(context).brightness == Brightness.dark
? AppConfig.primaryColorLight
: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.primary,
width: 2,
),
),

View file

@ -1,14 +1,12 @@
import 'dart:developer';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart';
import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class MorphMeaningWidget extends StatefulWidget {
final String feature;
@ -33,11 +31,24 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
late TextEditingController _controller;
static const int maxCharacters = 140;
String? _cachedResponse;
bool _isLoading = true;
String? _error;
@override
void didUpdateWidget(covariant MorphMeaningWidget oldWidget) {
if (oldWidget.tag != widget.tag || oldWidget.feature != widget.feature) {
_cachedResponse = null;
_isLoading = true;
_loadMorphMeaning();
}
super.didUpdateWidget(oldWidget);
}
@override
void initState() {
super.initState();
_controller = TextEditingController();
_loadMorphMeaning();
}
@override
@ -46,6 +57,19 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
super.dispose();
}
Future<void> _loadMorphMeaning() async {
try {
final response = await _morphMeaning();
_setMeaningText(response);
} catch (e) {
_error = e.toString();
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<String> _morphMeaning() async {
if (_cachedResponse != null) {
return _cachedResponse!;
@ -87,62 +111,56 @@ class MorphMeaningWidgetState extends State<MorphMeaningWidget> {
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _morphMeaning(),
builder: (context, snapshot) {
if (snapshot.hasData) {
_setMeaningText(snapshot.data!);
}
if (_isLoading) {
return const TextLoadingShimmer();
}
if (_editMode) {
return MorphEditView(
morphFeature: widget.feature,
morphTag: widget.tag,
meaning: snapshot.data ?? "",
controller: _controller,
toggleEditMode: _toggleEditMode,
editMorphMeaning: editMorphMeaning,
);
}
if (_error != null) {
debugger(when: kDebugMode);
return Text(
L10n.of(context).oopsSomethingWentWrong,
textAlign: TextAlign.center,
style: widget.style,
);
}
if (snapshot.connectionState != ConnectionState.done) {
return const TextLoadingShimmer();
}
if (_editMode) {
return MorphEditView(
morphFeature: widget.feature,
morphTag: widget.tag,
meaning: _cachedResponse ?? "",
controller: _controller,
toggleEditMode: _toggleEditMode,
editMorphMeaning: editMorphMeaning,
);
}
if (snapshot.hasError || snapshot.data == null) {
debugger(when: kDebugMode);
return Text(
L10n.of(context).oopsSomethingWentWrong,
textAlign: TextAlign.center,
style: widget.style,
);
}
return Row(
children: [
Flexible(
child: Tooltip(
triggerMode: TooltipTriggerMode.tap,
message: L10n.of(context).doubleClickToEdit,
child: GestureDetector(
onLongPress: () => _toggleEditMode(true),
onDoubleTap: () => _toggleEditMode(true),
child: RichText(
text: TextSpan(
style: widget.style,
children: [
if (widget.leading != null) widget.leading!,
if (widget.leading != null) const TextSpan(text: ' '),
TextSpan(text: snapshot.data!),
],
),
),
return Row(
mainAxisAlignment: widget.leading != null
? MainAxisAlignment.start
: MainAxisAlignment.center,
children: [
Flexible(
child: Tooltip(
triggerMode: TooltipTriggerMode.tap,
message: L10n.of(context).doubleClickToEdit,
child: GestureDetector(
onLongPress: () => _toggleEditMode(true),
onDoubleTap: () => _toggleEditMode(true),
child: RichText(
text: TextSpan(
style: widget.style,
children: [
if (widget.leading != null) widget.leading!,
if (widget.leading != null) const TextSpan(text: ' '),
TextSpan(text: _cachedResponse!),
],
),
),
),
],
);
},
),
),
],
);
}
}

View file

@ -1,12 +1,6 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_list_model.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
@ -23,6 +17,10 @@ import 'package:fluffychat/pangea/events/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
import 'package:fluffychat/pangea/practice_activities/message_analytics_controller.dart';
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
/// A minimized version of AnalyticsController that get the logged in user's analytics
class GetAnalyticsController extends BaseController {

View file

@ -1,13 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.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/practice_activities/message_analytics_controller.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_token_text.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import '../../../utils/matrix_sdk_extensions/matrix_locals.dart';
class ChatListItemSubtitle extends StatelessWidget {
@ -91,7 +90,6 @@ class ChatListItemSubtitle extends StatelessWidget {
final analyticsEntry = tokens != null
? MessageAnalyticsController.get(
tokens,
pangeaMessageEvent,
)
: null;

View file

@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import '../../../bot/widgets/bot_face_svg.dart';
class CardHeader extends StatelessWidget {
@ -49,9 +48,6 @@ class CardHeader extends StatelessWidget {
if (onClose != null) onClose!();
MatrixState.pAnyState.closeOverlay();
},
color: Theme.of(context).brightness == Brightness.dark
? AppConfig.primaryColorLight
: Theme.of(context).colorScheme.primary,
),
],
),

View file

@ -1,9 +1,6 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/analytics_misc/analytics_constants.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
@ -23,6 +20,8 @@ import 'package:fluffychat/pangea/morphs/parts_of_speech_enum.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import '../../common/constants/model_keys.dart';
import '../../lemmas/lemma.dart';
@ -640,9 +639,9 @@ class PangeaToken {
);
}
/// [0,infinity) - a lower number means higher priority
/// [0,infinity) - a higher number means higher priority
int activityPriorityScore(ActivityTypeEnum a, String? morphFeature) {
return daysSinceLastUseByType(a, morphFeature) *
(vocabConstructID.isContentWord ? 1 : 2);
(vocabConstructID.isContentWord ? 10 : 9);
}
}

View file

@ -1,9 +1,8 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.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/practice_activities/message_analytics_controller.dart';
import 'package:fluffychat/pangea/practice_activities/message_analytics_entry.dart';
import 'package:flutter/material.dart';
class TokenPosition {
/// Start index of the full substring in the message

View file

@ -1,10 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class InstructionsInlineTooltip extends StatefulWidget {
final InstructionsEnum instructionsEnum;
@ -29,7 +27,6 @@ class InstructionsInlineTooltipState extends State<InstructionsInlineTooltip>
@override
void didUpdateWidget(covariant InstructionsInlineTooltip oldWidget) {
debugPrint("InstructionsInlineTooltip didUpdateWidget");
if (oldWidget.instructionsEnum != widget.instructionsEnum) {
setToggled();
}

View file

@ -1,9 +1,6 @@
import 'dart:developer';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/app_emojis.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
@ -13,6 +10,8 @@ import 'package:fluffychat/pangea/lemmas/user_set_lemma_info.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class LemmaEmojiRow extends StatefulWidget {
final ConstructIdentifier cId;
@ -89,6 +88,7 @@ class LemmaEmojiRowState extends State<LemmaEmojiRow> {
blurBackground: false,
borderColor: Theme.of(context).colorScheme.primary,
closePrevOverlay: false,
offset: const Offset(0, 60),
);
} catch (e, s) {
debugger(when: kDebugMode);
@ -109,6 +109,11 @@ class LemmaEmojiRowState extends State<LemmaEmojiRow> {
.catchError((e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(data: widget.cId.toJson(), e: e, s: s);
}).then((_) {
if (mounted) {
widget.emojiSetCallback?.call();
setState(() {});
}
});
MatrixState.pAnyState.closeOverlay();
@ -124,46 +129,49 @@ class LemmaEmojiRowState extends State<LemmaEmojiRow> {
@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: MatrixState.pAnyState
.layerLinkAndKey(
widget.cId.string,
)
.link,
child: Container(
key: MatrixState.pAnyState
return Material(
child: CompositedTransformTarget(
link: MatrixState.pAnyState
.layerLinkAndKey(
widget.cId.string,
)
.key,
height: 50,
width: 50,
alignment: Alignment.center,
child: displayEmoji != null && widget.shouldShowEmojis
? InkWell(
hoverColor: Theme.of(context).colorScheme.primary.withAlpha(50),
onTap: widget.onTapOverride ?? openEmojiSetOverlay,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
displayEmoji!,
style: Theme.of(context).textTheme.headlineSmall,
),
),
.link,
child: Container(
key: MatrixState.pAnyState
.layerLinkAndKey(
widget.cId.string,
)
: WordZoomActivityButton(
icon: Icon(
Icons.add_reaction_outlined,
color: widget.isSelected
? Theme.of(context).colorScheme.primary
: null,
.key,
height: 50,
width: 50,
alignment: Alignment.center,
child: displayEmoji != null && widget.shouldShowEmojis
? InkWell(
hoverColor:
Theme.of(context).colorScheme.primary.withAlpha(50),
onTap: widget.onTapOverride ?? openEmojiSetOverlay,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
displayEmoji!,
style: Theme.of(context).textTheme.headlineSmall,
),
),
)
: WordZoomActivityButton(
icon: Icon(
Icons.add_reaction_outlined,
color: widget.isSelected
? Theme.of(context).colorScheme.primary
: null,
),
isSelected: widget.isSelected,
onPressed: widget.onTapOverride ?? openEmojiSetOverlay,
opacity: widget.isSelected ? 1 : 0.4,
tooltip: MessageMode.wordEmoji.title(context),
),
isSelected: widget.isSelected,
onPressed: widget.onTapOverride ?? openEmojiSetOverlay,
opacity: widget.isSelected ? 1 : 0.4,
tooltip: MessageMode.wordEmoji.title(context),
),
),
),
);
}
@ -184,6 +192,7 @@ class EmojiEditOverlay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Container(
padding: const EdgeInsets.all(8),
height: 70,

View file

@ -1,20 +1,19 @@
import 'dart:developer';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/message_token_text/dotted_border_painter.dart';
import 'package:fluffychat/pangea/practice_activities/target_tokens_and_activity_type.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_selection.dart';
import 'package:fluffychat/pangea/toolbar/utils/shrinkable_text.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
const double tokenButtonHeight = 40.0;
const double tokenButtonDefaultFontSize = 10;
@ -155,23 +154,26 @@ class MessageTokenButtonState extends State<MessageTokenButton>
}
return InkWell(
onHover: (isHovered) => setState(() => _isHovered = isHovered),
onTap: () => widget.overlayController!
.onMorphActivitySelect(widget.token, activity!.morphFeature!),
onTap: () => widget.overlayController!.onMorphActivitySelect(
MorphSelection(widget.token, activity!.morphFeature!),
),
borderRadius: borderRadius,
child: Container(
height: height,
width: min(widget.width, height),
alignment: Alignment.center,
child: Opacity(
opacity: (widget.overlayController?.selectedToken == widget.token &&
widget.overlayController?.selectedMorph ==
opacity: (widget.overlayController?.selectedMorph?.token ==
widget.token &&
widget.overlayController?.selectedMorph?.morph ==
activity?.morphFeature) ||
_isHovered
? 1.0
: 0.5,
: 0.4,
child: Icon(
Symbols.toys_and_games,
color: Theme.of(context).colorScheme.primary,
size: min(24, widget.width),
),
// MorphIcon(morphFeature: activity!.morphFeature!, morphTag: null),
),

View file

@ -1,301 +1,76 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:collection/collection.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/lemmas/lemma_info_response.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/target_tokens_and_activity_type.dart';
import 'package:fluffychat/pangea/practice_activities/message_analytics_entry.dart';
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
class MessageAnalyticsEntry {
final DateTime createdAt = DateTime.now();
late final List<PangeaToken> _tokens;
final Map<ActivityTypeEnum, List<TargetTokensAndActivityType>>
_activityQueue = {};
final int _maxQueueLength = 5;
MessageAnalyticsEntry({
required List<PangeaToken> tokens,
required bool includeHiddenWordActivities,
required PangeaMessageEvent pangeaMessageEvent,
}) {
_tokens = tokens;
initialize();
}
void _pushQueue(TargetTokensAndActivityType entry) {
if (_activityQueue.containsKey(entry.activityType)) {
_activityQueue[entry.activityType]!.insert(0, entry);
} else {
_activityQueue[entry.activityType] = [entry];
}
// just in case we make a mistake and the queue gets too long
if (_activityQueue[entry.activityType]!.length > _maxQueueLength) {
debugger(when: kDebugMode);
_activityQueue[entry.activityType]!.removeRange(
_maxQueueLength,
_activityQueue.length,
);
}
}
TargetTokensAndActivityType? nextActivity(ActivityTypeEnum a) =>
_activityQueue[a]?.firstOrNull;
bool get hasHiddenWordActivity =>
activities(ActivityTypeEnum.hiddenWordListening).isNotEmpty;
bool get hasMessageMeaningActivity =>
activities(ActivityTypeEnum.messageMeaning).isNotEmpty;
int get numActivities => _activityQueue.length;
List<TargetTokensAndActivityType> activities(ActivityTypeEnum a) =>
_activityQueue[a] ?? [];
// /// If there are more than 4 tokens that can be heard, we don't want to do word focus listening
// /// Otherwise, we don't have enough distractors
// bool get canDoWordFocusListening =>
// _tokens.where((t) => t.canBeHeard).length > 4;
/// On initialization, we pick which tokens to do activities on and what types of activities to do
void initialize() {
final eligibleTokens = _tokens.where((t) => t.lemma.saveVocab);
// EMOJI
// sort the tokens by the preference of them for an emoji activity
// order from least to most recent
// words that have never been used are counted as 1000 days
// we preference content words over function words by multiplying the days since last use by 2
// NOTE: for now, we put it at the end if it has no uses and basically just give them the answer
// later on, we may introduce an emoji activity that is easier than the current matching one
// i.e. we show them 3 good emojis and 1 bad one and ask them to pick the bad one
_activityQueue[ActivityTypeEnum.emoji] = eligibleTokens
.map(
(t) => TargetTokensAndActivityType(
tokens: [t],
activityType: ActivityTypeEnum.emoji,
),
)
.sorted(
(a, b) => a.tokens.first
.activityPriorityScore(ActivityTypeEnum.emoji, null)
.compareTo(
b.tokens.first
.activityPriorityScore(ActivityTypeEnum.emoji, null),
),
)
.take(_maxQueueLength)
.shuffled()
.toList();
// WORD MEANING
// make word meaning activities
// same as emojis for now
_activityQueue[ActivityTypeEnum.wordMeaning] = eligibleTokens
.map(
(t) => TargetTokensAndActivityType(
tokens: [t],
activityType: ActivityTypeEnum.wordMeaning,
),
)
.sorted(
(a, b) => a.tokens.first
.activityPriorityScore(ActivityTypeEnum.wordMeaning, null)
.compareTo(
b.tokens.first
.activityPriorityScore(ActivityTypeEnum.wordMeaning, null),
),
)
.take(_maxQueueLength)
.shuffled()
.toList();
// WORD FOCUS LISTENING
// make word focus listening activities
// same as emojis for now
_activityQueue[ActivityTypeEnum.wordFocusListening] = eligibleTokens
.map(
(t) => TargetTokensAndActivityType(
tokens: [t],
activityType: ActivityTypeEnum.wordFocusListening,
),
)
.sorted(
(a, b) => a.tokens.first
.activityPriorityScore(ActivityTypeEnum.wordFocusListening, null)
.compareTo(
b.tokens.first.activityPriorityScore(
ActivityTypeEnum.wordFocusListening,
null,
),
),
)
.take(_maxQueueLength)
.shuffled()
.toList();
// GRAMMAR
// build a list of TargetTokensAndActivityType for all tokens and all features in the message
// limits to _maxQueueLength activities and only one per token
final List<TargetTokensAndActivityType> candidates = eligibleTokens.expand(
(t) {
return t.morphsBasicallyEligibleForPracticeByPriority.map(
(m) => TargetTokensAndActivityType(
tokens: [t],
activityType: ActivityTypeEnum.morphId,
morphFeature: MorphFeaturesEnumExtension.fromString(m.category),
),
);
},
).sorted(
(a, b) => a.tokens.first
.activityPriorityScore(
ActivityTypeEnum.morphId,
a.morphFeature!.name,
)
.compareTo(
b.tokens.first.activityPriorityScore(
ActivityTypeEnum.morphId,
b.morphFeature!.name,
),
),
);
//pick from the top 5, only including one per token
_activityQueue[ActivityTypeEnum.morphId] = [];
for (final candidate in candidates) {
if (_activityQueue[ActivityTypeEnum.morphId]!.length >= _maxQueueLength) {
break;
}
if (_activityQueue[ActivityTypeEnum.morphId]?.any(
(entry) => entry.tokens.contains(candidate.tokens.first),
) ==
false) {
_activityQueue[ActivityTypeEnum.morphId]?.add(candidate);
}
}
}
bool hasActivity(
ActivityTypeEnum a,
PangeaToken t, [
MorphFeaturesEnum? morph,
]) =>
_activityQueue[a]?.any(
(entry) =>
entry.tokens.contains(t) &&
(morph == null || entry.morphFeature == morph),
) ==
true;
/// Add a message meaning activity to the front of the queue
/// And limits to _maxQueueLength activities
void addMessageMeaningActivity() {
final entry = TargetTokensAndActivityType(
tokens: _tokens,
activityType: ActivityTypeEnum.messageMeaning,
);
_pushQueue(entry);
}
void onActivityComplete(ActivityTypeEnum a, PangeaToken? token) {
_activityQueue[a]
?.removeWhere((entry) => token == null || entry.tokens.contains(token));
}
void exitPracticeFlow() => _activityQueue.clear();
void revealAllTokens() =>
_activityQueue[ActivityTypeEnum.hiddenWordListening]?.clear();
bool isTokenInHiddenWordActivity(PangeaToken token) =>
_activityQueue[ActivityTypeEnum.hiddenWordListening]?.isNotEmpty ?? false;
Future<List<LemmaInfoResponse>> getLemmaInfoForActivityTokens() async {
// make a list of unique tokens in emoji and wordMeaning activities
final List<PangeaToken> uniqueTokens = [];
for (final t in _activityQueue[ActivityTypeEnum.emoji] ?? []) {
if (!uniqueTokens.contains(t.tokens.first)) {
uniqueTokens.add(t.tokens.first);
}
}
for (final t in _activityQueue[ActivityTypeEnum.wordMeaning] ?? []) {
if (!uniqueTokens.contains(t.tokens.first)) {
uniqueTokens.add(t.tokens.first);
}
}
// get the lemma info for each token
final List<Future<LemmaInfoResponse>> lemmaInfoFutures = [];
for (final t in uniqueTokens) {
lemmaInfoFutures.add(t.vocabConstructID.getLemmaInfo());
}
return Future.wait(lemmaInfoFutures);
}
}
/// computes TokenWithXP for given a pangeaMessageEvent and caches the result, according to the full text of the message
/// listens for analytics updates and updates the cache accordingly
class MessageAnalyticsController {
static final Map<String, MessageAnalyticsEntry> _cache = {};
static final GetStorage _storage = GetStorage('message_analytics_cache');
static final Map<String, MessageAnalyticsEntry> _memoryCache = {};
static const int _maxMemoryCacheSize = 50;
void dispose() {
_cache.clear();
_storage.erase();
_memoryCache.clear();
}
static void save(MessageAnalyticsEntry entry) {
final key = _key(entry.tokens);
_storage.write(key, entry.toJson());
_memoryCache[key] = entry;
}
// if over 300, remove oldest 5 entries by createdAt
static void clean() {
if (_cache.length > 300) {
final sortedEntries = _cache.entries.toList()
final Iterable<String> keys = _storage.getKeys();
if (keys.length > 300) {
final entries = keys
.map((key) {
final entry = MessageAnalyticsEntry.fromJson(_storage.read(key));
return MapEntry(key, entry);
})
.cast<MapEntry<String, MessageAnalyticsEntry>>()
.toList()
..sort((a, b) => a.value.createdAt.compareTo(b.value.createdAt));
for (var i = 0; i < 5; i++) {
_cache.remove(sortedEntries[i].key);
_storage.remove(entries[i].key);
}
}
if (_memoryCache.length > _maxMemoryCacheSize) {
_memoryCache.remove(_memoryCache.keys.first);
}
}
static String _key(List<PangeaToken> tokens) =>
PangeaToken.reconstructText(tokens);
tokens.map((t) => t.text.content).join(' ');
static MessageAnalyticsEntry? get(
List<PangeaToken> tokens,
PangeaMessageEvent pangeaMessageEvent,
) {
final String key = _key(tokens);
final entry = _cache[key];
// if cache is older than 1 day, then remove and recompute
if (entry != null &&
DateTime.now().difference(entry.createdAt).inDays > 1) {
_cache.remove(key);
if (_memoryCache.containsKey(key)) {
return _memoryCache[key];
}
if (entry != null) {
return entry;
final entryJson = _storage.read(key);
if (entryJson != null) {
final entry = MessageAnalyticsEntry.fromJson(entryJson);
if (DateTime.now().difference(entry.createdAt).inDays > 1) {
debugPrint('removing old entry ${entry.createdAt}');
_storage.remove(key);
} else {
_memoryCache[key] = entry;
return entry;
}
}
final bool includeHiddenWordActivities = !pangeaMessageEvent.ownMessage &&
pangeaMessageEvent.messageDisplayRepresentation?.tokens != null &&
pangeaMessageEvent.messageDisplayLangIsL2 &&
!pangeaMessageEvent.event.isRichMessage;
_cache[key] = MessageAnalyticsEntry(
final newEntry = MessageAnalyticsEntry(
tokens: tokens,
includeHiddenWordActivities: includeHiddenWordActivities,
pangeaMessageEvent: pangeaMessageEvent,
);
_storage.write(key, newEntry.toJson());
_memoryCache[key] = newEntry;
clean();
return _cache[key];
return newEntry;
}
}

View file

@ -0,0 +1,284 @@
import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_response.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_analytics_controller.dart';
import 'package:fluffychat/pangea/practice_activities/target_tokens_and_activity_type.dart';
import 'package:flutter/foundation.dart';
class MessageAnalyticsEntry {
final DateTime createdAt = DateTime.now();
late final List<PangeaToken> _tokens;
final Map<ActivityTypeEnum, List<TargetTokensAndActivityType>>
_activityQueue = {};
final int _maxQueueLength = 5;
MessageAnalyticsEntry({
required List<PangeaToken> tokens,
}) {
_tokens = tokens;
initialize();
}
List<PangeaToken> get tokens => _tokens;
Map<String, dynamic> toJson() => {
'createdAt': createdAt.toIso8601String(),
'tokens': _tokens.map((t) => t.toJson()).toList(),
'activityQueue': _activityQueue.map(
(key, value) => MapEntry(
key.toString(),
value.map((e) => e.toJson()).toList(),
),
),
};
static MessageAnalyticsEntry fromJson(Map<String, dynamic> json) {
return MessageAnalyticsEntry(
tokens:
(json['tokens'] as List).map((t) => PangeaToken.fromJson(t)).toList(),
).._activityQueue.addAll(
(json['activityQueue'] as Map<String, dynamic>).map(
(key, value) => MapEntry(
ActivityTypeEnum.values.firstWhere((e) => e.toString() == key),
(value as List)
.map((e) => TargetTokensAndActivityType.fromJson(e))
.toList(),
),
),
);
}
void _pushQueue(TargetTokensAndActivityType entry) {
if (_activityQueue.containsKey(entry.activityType)) {
_activityQueue[entry.activityType]!.insert(0, entry);
} else {
_activityQueue[entry.activityType] = [entry];
}
// just in case we make a mistake and the queue gets too long
if (_activityQueue[entry.activityType]!.length > _maxQueueLength) {
debugger(when: kDebugMode);
_activityQueue[entry.activityType]!.removeRange(
_maxQueueLength,
_activityQueue.length,
);
}
}
TargetTokensAndActivityType? nextActivity(ActivityTypeEnum a) =>
_activityQueue[a]?.firstOrNull;
bool get hasHiddenWordActivity =>
activities(ActivityTypeEnum.hiddenWordListening).isNotEmpty;
bool get hasMessageMeaningActivity =>
activities(ActivityTypeEnum.messageMeaning).isNotEmpty;
int get numActivities => _activityQueue.length;
List<TargetTokensAndActivityType> activities(ActivityTypeEnum a) =>
_activityQueue[a] ?? [];
// /// If there are more than 4 tokens that can be heard, we don't want to do word focus listening
// /// Otherwise, we don't have enough distractors
// bool get canDoWordFocusListening =>
// _tokens.where((t) => t.canBeHeard).length > 4;
/// On initialization, we pick which tokens to do activities on and what types of activities to do
void initialize() {
final eligibleTokens = _tokens.where((t) => t.lemma.saveVocab);
// EMOJI
// sort the tokens by the preference of them for an emoji activity
// order from least to most recent
// words that have never been used are counted as 1000 days
// we preference content words over function words by multiplying the days since last use by 2
// NOTE: for now, we put it at the end if it has no uses and basically just give them the answer
// later on, we may introduce an emoji activity that is easier than the current matching one
// i.e. we show them 3 good emojis and 1 bad one and ask them to pick the bad one
_activityQueue[ActivityTypeEnum.emoji] = eligibleTokens
.map(
(t) => TargetTokensAndActivityType(
tokens: [t],
activityType: ActivityTypeEnum.emoji,
),
)
.sorted(
(a, b) => b.tokens.first
.activityPriorityScore(ActivityTypeEnum.emoji, null)
.compareTo(
a.tokens.first
.activityPriorityScore(ActivityTypeEnum.emoji, null),
),
);
debugPrint(
'emoji activity priority score: ${_activityQueue[ActivityTypeEnum.emoji]!.map(
(e) => e.tokens.first.activityPriorityScore(ActivityTypeEnum.emoji, null),
)}');
_activityQueue[ActivityTypeEnum.emoji] =
_activityQueue[ActivityTypeEnum.emoji]!
.take(_maxQueueLength)
.shuffled()
.toList();
// WORD MEANING
// make word meaning activities
// same as emojis for now
_activityQueue[ActivityTypeEnum.wordMeaning] = eligibleTokens
.map(
(t) => TargetTokensAndActivityType(
tokens: [t],
activityType: ActivityTypeEnum.wordMeaning,
),
)
.sorted(
(a, b) => b.tokens.first
.activityPriorityScore(ActivityTypeEnum.wordMeaning, null)
.compareTo(
a.tokens.first
.activityPriorityScore(ActivityTypeEnum.wordMeaning, null),
),
)
.take(_maxQueueLength)
.shuffled()
.toList();
// WORD FOCUS LISTENING
// make word focus listening activities
// same as emojis for now
_activityQueue[ActivityTypeEnum.wordFocusListening] = eligibleTokens
.map(
(t) => TargetTokensAndActivityType(
tokens: [t],
activityType: ActivityTypeEnum.wordFocusListening,
),
)
.sorted(
(a, b) => b.tokens.first
.activityPriorityScore(ActivityTypeEnum.wordFocusListening, null)
.compareTo(
a.tokens.first.activityPriorityScore(
ActivityTypeEnum.wordFocusListening,
null,
),
),
)
.take(_maxQueueLength)
.shuffled()
.toList();
// GRAMMAR
// build a list of TargetTokensAndActivityType for all tokens and all features in the message
// limits to _maxQueueLength activities and only one per token
final List<TargetTokensAndActivityType> candidates = eligibleTokens.expand(
(t) {
return t.morphsBasicallyEligibleForPracticeByPriority.map(
(m) => TargetTokensAndActivityType(
tokens: [t],
activityType: ActivityTypeEnum.morphId,
morphFeature: MorphFeaturesEnumExtension.fromString(m.category),
),
);
},
).sorted(
(a, b) => b.tokens.first
.activityPriorityScore(
ActivityTypeEnum.morphId,
b.morphFeature!.name,
)
.compareTo(
a.tokens.first.activityPriorityScore(
ActivityTypeEnum.morphId,
a.morphFeature!.name,
),
),
);
//pick from the top 5, only including one per token
_activityQueue[ActivityTypeEnum.morphId] = [];
for (final candidate in candidates) {
if (_activityQueue[ActivityTypeEnum.morphId]!.length >= _maxQueueLength) {
break;
}
if (_activityQueue[ActivityTypeEnum.morphId]?.any(
(entry) => entry.tokens.contains(candidate.tokens.first),
) ==
false) {
_activityQueue[ActivityTypeEnum.morphId]?.add(candidate);
}
}
MessageAnalyticsController.save(this);
}
bool hasActivity(
ActivityTypeEnum a,
PangeaToken t, [
MorphFeaturesEnum? morph,
]) =>
_activityQueue[a]?.any(
(entry) =>
entry.tokens.contains(t) &&
(morph == null || entry.morphFeature == morph),
) ==
true;
/// Add a message meaning activity to the front of the queue
/// And limits to _maxQueueLength activities
void addMessageMeaningActivity() {
final entry = TargetTokensAndActivityType(
tokens: _tokens,
activityType: ActivityTypeEnum.messageMeaning,
);
_pushQueue(entry);
}
void onActivityComplete(ActivityTypeEnum a, PangeaToken? token) {
_activityQueue[a]
?.removeWhere((entry) => token == null || entry.tokens.contains(token));
MessageAnalyticsController.save(this);
}
void exitPracticeFlow() {
_activityQueue.clear();
MessageAnalyticsController.save(this);
}
void revealAllTokens() {
_activityQueue[ActivityTypeEnum.hiddenWordListening]?.clear();
MessageAnalyticsController.save(this);
}
bool isTokenInHiddenWordActivity(PangeaToken token) =>
_activityQueue[ActivityTypeEnum.hiddenWordListening]?.isNotEmpty ?? false;
Future<List<LemmaInfoResponse>> getLemmaInfoForActivityTokens() async {
// make a list of unique tokens in emoji and wordMeaning activities
final List<PangeaToken> uniqueTokens = [];
for (final t in _activityQueue[ActivityTypeEnum.emoji] ?? []) {
if (!uniqueTokens.contains(t.tokens.first)) {
uniqueTokens.add(t.tokens.first);
}
}
for (final t in _activityQueue[ActivityTypeEnum.wordMeaning] ?? []) {
if (!uniqueTokens.contains(t.tokens.first)) {
uniqueTokens.add(t.tokens.first);
}
}
// get the lemma info for each token
final List<Future<LemmaInfoResponse>> lemmaInfoFutures = [];
for (final t in uniqueTokens) {
lemmaInfoFutures.add(t.vocabConstructID.getLemmaInfo());
}
return Future.wait(lemmaInfoFutures);
}
}

View file

@ -1,8 +1,7 @@
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:flutter/foundation.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
@ -46,4 +45,23 @@ class TargetTokensAndActivityType {
@override
int get hashCode =>
tokens.hashCode ^ activityType.hashCode ^ morphFeature.hashCode;
static TargetTokensAndActivityType fromJson(Map<String, dynamic> json) {
return TargetTokensAndActivityType(
tokens:
(json['tokens'] as List).map((e) => PangeaToken.fromJson(e)).toList(),
activityType: ActivityTypeEnum.values[json['activityType']],
morphFeature: json['morphFeature'] == null
? null
: MorphFeaturesEnum.values[json['morphFeature']],
);
}
Map<String, dynamic> toJson() {
return {
'tokens': tokens.map((e) => e.toJson()).toList(),
'activityType': activityType.index,
'morphFeature': morphFeature?.index,
};
}
}

View file

@ -1,8 +1,5 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
@ -11,6 +8,8 @@ import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_match_activity_item.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_audio_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class MessageMatchActivity extends StatelessWidget {
final MessageOverlayController overlayController;

View file

@ -1,16 +1,14 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/constructs/construct_form.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/match_feedback_model.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class MessageMatchActivityItem extends StatefulWidget {
const MessageMatchActivityItem({
@ -88,14 +86,14 @@ class MessageMatchActivityItemState extends State<MessageMatchActivityItem> {
}
if (isSelected) {
return AppConfig.primaryColor;
return Theme.of(context).colorScheme.primaryContainer;
}
if (_isHovered) {
return Theme.of(context).colorScheme.primary;
return Theme.of(context).colorScheme.primaryContainer;
}
return Colors.transparent;
return Theme.of(context).colorScheme.surface;
}
@override
@ -147,12 +145,6 @@ class MessageMatchActivityItemState extends State<MessageMatchActivityItem> {
onDragStarted: () {
widget.overlayController.onChoiceSelect(widget.constructForm, true);
},
// onDragCompleted: () {
// debugger(when: kDebugMode);
// },
// onDragEnd: (details) {
// // debugger(when: kDebugMode);
// },
child: InkWell(
onHover: (isHovered) => setState(() => _isHovered = isHovered),
borderRadius: BorderRadius.circular(AppConfig.borderRadius),

View file

@ -1,11 +1,6 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_gen/gen_l10n/l10n.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';
@ -23,6 +18,9 @@ import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/message_m
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/morph_focus_widget.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
// this widget will handle the content of the input bar when mode == MessageMode.wordMorph
@ -63,8 +61,8 @@ class MessageMorphInputBarContentState
// }
MessageOverlayController get overlay => widget.overlayController;
PangeaToken? get token => overlay.selectedToken;
MorphFeaturesEnum? get morph => overlay.selectedMorph;
PangeaToken? get token => overlay.selectedMorph?.token;
MorphFeaturesEnum? get morph => overlay.selectedMorph?.morph;
// void init() async {
// initialized = false;

View file

@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/constructs/construct_identifier.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';
import 'package:flutter/material.dart';
class MessageMorphChoiceItem extends StatefulWidget {
const MessageMorphChoiceItem({
@ -47,10 +46,13 @@ class MessageMorphChoiceItemState extends State<MessageMorphChoiceItem> {
: AppConfig.warning.withAlpha((0.4 * 255).toInt());
}
if (widget.isSelected) {
return AppConfig.primaryColor.withAlpha((0.4 * 255).toInt());
return Theme.of(context)
.colorScheme
.primary
.withAlpha((0.4 * 255).toInt());
}
return _isHovered
? AppConfig.primaryColor.withAlpha((0.2 * 255).toInt())
? Theme.of(context).colorScheme.primary.withAlpha((0.2 * 255).toInt())
: Colors.transparent;
}

View file

@ -0,0 +1,24 @@
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
class MorphSelection {
PangeaToken token;
MorphFeaturesEnum morph;
MorphSelection(
this.token,
this.morph,
);
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is MorphSelection &&
other.token == token &&
other.morph == morph;
}
@override
int get hashCode => token.hashCode ^ morph.hashCode;
}

View file

@ -1,8 +1,5 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/analytics_misc/put_analytics_controller.dart';
@ -17,6 +14,10 @@ import 'package:fluffychat/pangea/toolbar/widgets/message_mode_locked_card.dart'
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_translation_card.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
const double minContentHeight = 120;
class ReadingAssistanceInputBar extends StatelessWidget {
final ChatController controller;
@ -56,6 +57,7 @@ class ReadingAssistanceInputBar extends StatelessWidget {
}
Widget barContent(BuildContext context) {
Widget? content;
switch (overlayController.toolbarMode) {
// message meaning will not use the input bar (for now at least)
// maybe we move some choices there later
@ -64,7 +66,7 @@ class ReadingAssistanceInputBar extends StatelessWidget {
case MessageMode.wordZoom:
case MessageMode.noneSelected:
//TODO: show all emojis for the lemmas and allow sending normal reactions
return const SizedBox.shrink();
break;
// return MessageEmojiChoice(
// controller: controller,
// overlayController: overlayController,
@ -72,27 +74,38 @@ class ReadingAssistanceInputBar extends StatelessWidget {
case MessageMode.messageTranslation:
if (overlayController.isTranslationUnlocked) {
return MessageTranslationCard(
content = MessageTranslationCard(
messageEvent: overlayController.pangeaMessageEvent!,
);
} else {
return MessageModeLockedCard(controller: overlayController);
content = MessageModeLockedCard(controller: overlayController);
}
case MessageMode.wordEmoji:
case MessageMode.messageMeaning:
case MessageMode.wordMeaning:
case MessageMode.listening:
return MessageMatchActivity(
content = MessageMatchActivity(
overlayController: overlayController,
);
case MessageMode.wordMorph:
return MessageMorphInputBarContent(
content = MessageMorphInputBarContent(
overlayController: overlayController,
pangeaMessageEvent: overlayController.pangeaMessageEvent!,
);
}
if (content == null) {
return const SizedBox();
}
return Container(
constraints: const BoxConstraints(
minHeight: minContentHeight,
),
child: content,
);
}
@override

View file

@ -1,19 +1,17 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/events/audio_player.dart';
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/card_error_widget.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_content_loading_indicator.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
class MessageAudioCard extends StatefulWidget {
final PangeaMessageEvent messageEvent;
@ -86,8 +84,9 @@ class MessageAudioCardState extends State<MessageAudioCard> {
children: [
Container(
alignment: Alignment.center,
height: 40,
child: _isLoading
? const ToolbarContentLoadingIndicator()
? const TextLoadingShimmer(width: 200)
: audioFile != null
? AudioPlayerWidget(
null,

View file

@ -1,13 +1,7 @@
import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:collection/collection.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
@ -25,15 +19,20 @@ import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart';
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart';
import 'package:fluffychat/pangea/lemmas/lemma_info_response.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_analytics_controller.dart';
import 'package:fluffychat/pangea/practice_activities/message_analytics_entry.dart';
import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/match_feedback_model.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_selection.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_positioner.dart';
import 'package:fluffychat/pangea/toolbar/widgets/reading_assistance_content.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:matrix/matrix.dart';
/// Controls data at the top level of the toolbar (mainly token / toolbar mode selection)
class MessageSelectionOverlay extends StatefulWidget {
@ -72,7 +71,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
Map<ConstructIdentifier, LemmaInfoResponse>? messageLemmaInfos;
MorphFeaturesEnum? selectedMorph;
MorphSelection? selectedMorph;
ConstructForm? selectedChoice;
PangeaTokenText? _selectedSpan;
@ -108,7 +107,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
Future<void> initializeTokensAndMode() async {
try {
debugPrint("what");
RepresentationEvent? repEvent =
pangeaMessageEvent?.messageDisplayRepresentation;
repEvent ??= await _fetchNewRepEvent();
@ -323,12 +321,12 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
setState(() {});
}
void onMorphActivitySelect(PangeaToken token, MorphFeaturesEnum morph) {
void onMorphActivitySelect(MorphSelection newMorph) {
if (toolbarMode != MessageMode.wordMorph) {
updateToolbarMode(MessageMode.wordMorph);
}
selectedMorph = morph;
_updateSelectedSpan(token.text, true);
selectedMorph = newMorph;
setState(() {});
}
// only used for word meaning, emoji, and word focus listening atm
@ -338,6 +336,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
) async {
final ActivityTypeEnum activityType = toolbarMode.associatedActivityType!;
//TODO - account for some emojis being the same for multiple words
final bool isCorrect = token.vocabConstructID == choice.cId;
final ConstructUseTypeEnum? useType =
@ -457,7 +456,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
pangeaMessageEvent?.messageDisplayRepresentation?.tokens != null
? MessageAnalyticsController.get(
pangeaMessageEvent!.messageDisplayRepresentation!.tokens!,
pangeaMessageEvent!,
)
: null;

View file

@ -1,10 +1,6 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/config/themes.dart';
@ -23,6 +19,8 @@ import 'package:fluffychat/pangea/toolbar/widgets/overlay_center_content.dart';
import 'package:fluffychat/pangea/toolbar/widgets/overlay_header.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
/// Controls positioning of the message overlay.
class MessageSelectionPositioner extends StatefulWidget {
@ -552,9 +550,9 @@ class MessageSelectionPositionerState extends State<MessageSelectionPositioner>
_headerHeight,
),
child: Container(
constraints: const BoxConstraints(
constraints: BoxConstraints(
minWidth: 200.0,
maxWidth: 400.0,
maxWidth: _toolbarMaxWidth,
),
child: InstructionsInlineTooltip(
instructionsEnum:

View file

@ -1,8 +1,4 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
@ -10,10 +6,13 @@ import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/events/utils/message_text_util.dart';
import 'package:fluffychat/pangea/message_token_text/message_token_button.dart';
import 'package:fluffychat/pangea/practice_activities/message_analytics_controller.dart';
import 'package:fluffychat/pangea/practice_activities/message_analytics_entry.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/utils/url_launcher.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
/// Question - does this need to be stateful or does this work?
/// Need to test.
@ -56,7 +55,6 @@ class MessageTokenText extends StatelessWidget {
MessageAnalyticsEntry? get messageAnalyticsEntry => _tokens != null
? MessageAnalyticsController.get(
_tokens!,
_pangeaMessageEvent,
)
: null;
@ -184,7 +182,7 @@ class MessageTextWidget extends StatelessWidget {
return textPainter.width;
}
Color backgroundColor(TokenPosition tokenPosition) {
Color backgroundColor(BuildContext context, TokenPosition tokenPosition) {
final hideTokenHighlights = messageAnalyticsEntry != null &&
(messageAnalyticsEntry!.hasHiddenWordActivity ||
messageAnalyticsEntry!.hasMessageMeaningActivity);
@ -193,7 +191,7 @@ class MessageTextWidget extends StatelessWidget {
if (!hideTokenHighlights) {
if (tokenPosition.selected) {
backgroundColor = AppConfig.primaryColor;
backgroundColor = Theme.of(context).colorScheme.primary;
}
// else if (tokenPosition.isHighlighted) {
// backgroundColor = AppConfig.success.withAlpha(80);
@ -378,7 +376,7 @@ class MessageTextWidget extends StatelessWidget {
: 0,
width: tokenWidth,
child: Container(
color: backgroundColor(tokenPosition),
color: backgroundColor(context, tokenPosition),
),
),
],

View file

@ -1,11 +1,9 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/toolbar_button.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
class ToolbarButtonRow extends StatelessWidget {
final MessageOverlayController overlayController;
@ -35,71 +33,65 @@ class ToolbarButtonRow extends StatelessWidget {
return SizedBox(
height: AppConfig.toolbarButtonsHeight,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
spacing: 4.0,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
ToolbarButton(
mode: MessageMode.messageTranslation,
overlayController: overlayController,
onPressed: overlayController.updateToolbarMode,
buttonSize: buttonSize,
),
],
Container(
width: buttonSize + 4,
height: buttonSize + 4,
alignment: Alignment.center,
child: ToolbarButton(
mode: MessageMode.listening,
overlayController: overlayController,
onPressed: overlayController.updateToolbarMode,
buttonSize: buttonSize,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
spacing: 4.0,
children: [
Container(
width: buttonSize + 4,
height: buttonSize + 4,
alignment: Alignment.center,
child: ToolbarButton(
mode: MessageMode.wordMorph,
overlayController: overlayController,
onPressed: overlayController.updateToolbarMode,
buttonSize: buttonSize,
),
),
Container(
width: buttonSize + 4,
height: buttonSize + 4,
alignment: Alignment.center,
child: ToolbarButton(
mode: MessageMode.wordMeaning,
overlayController: overlayController,
onPressed: overlayController.updateToolbarMode,
buttonSize: buttonSize,
),
),
Container(
width: buttonSize + 4,
height: buttonSize + 4,
alignment: Alignment.center,
child: ToolbarButton(
mode: MessageMode.listening,
overlayController: overlayController,
onPressed: overlayController.updateToolbarMode,
buttonSize: buttonSize,
),
),
Container(
width: buttonSize + 4,
height: buttonSize + 4,
alignment: Alignment.center,
child: ToolbarButton(
mode: MessageMode.wordEmoji,
overlayController: overlayController,
onPressed: overlayController.updateToolbarMode,
buttonSize: buttonSize,
),
),
],
Container(
width: buttonSize + 4,
height: buttonSize + 4,
alignment: Alignment.center,
child: ToolbarButton(
mode: MessageMode.wordMorph,
overlayController: overlayController,
onPressed: overlayController.updateToolbarMode,
buttonSize: buttonSize,
),
),
Container(
width: buttonSize + 4,
height: buttonSize + 4,
alignment: Alignment.center,
child: ToolbarButton(
mode: MessageMode.messageTranslation,
overlayController: overlayController,
onPressed: overlayController.updateToolbarMode,
buttonSize: buttonSize,
),
),
Container(
width: buttonSize + 4,
height: buttonSize + 4,
alignment: Alignment.center,
child: ToolbarButton(
mode: MessageMode.wordMeaning,
overlayController: overlayController,
onPressed: overlayController.updateToolbarMode,
buttonSize: buttonSize,
),
),
Container(
width: buttonSize + 4,
height: buttonSize + 4,
alignment: Alignment.center,
child: ToolbarButton(
mode: MessageMode.wordEmoji,
overlayController: overlayController,
onPressed: overlayController.updateToolbarMode,
buttonSize: buttonSize,
),
),
],
),

View file

@ -170,6 +170,7 @@ class MorphFocusWidgetState extends State<MorphFocusWidget> {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 8.0,
children: [
MorphFeatureDisplay(
morphFeature: widget.morphFeature,
@ -214,6 +215,7 @@ class MorphFocusWidgetState extends State<MorphFocusWidget> {
MorphMeaningWidget(
feature: widget.morphFeature,
tag: widget.token.getMorphTag(widget.morphFeature)!,
style: Theme.of(context).textTheme.bodyLarge,
),
] else
Text(L10n.of(context).nan),

View file

@ -1,15 +1,14 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.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';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/reading_assistance_input_row/morph_selection.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
import 'package:fluffychat/pangea/toolbar/widgets/practice_activity/word_zoom_activity_button.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
class MorphologicalListItem extends StatelessWidget {
final MorphFeaturesEnum morphFeature;
@ -31,7 +30,9 @@ class MorphologicalListItem extends StatelessWidget {
) ==
true;
bool get isSelected => overlayController.toolbarMode == MessageMode.wordMorph;
bool get isSelected =>
overlayController.toolbarMode == MessageMode.wordMorph &&
overlayController.selectedMorph?.morph == morphFeature;
String get morphTag => token.getMorphTag(morphFeature.name) ?? "X";
@ -58,8 +59,8 @@ class MorphologicalListItem extends StatelessWidget {
// view: ConstructTypeEnum.vocab,
// ),
// ),
onPressed: () =>
overlayController.onMorphActivitySelect(token, morphFeature),
onPressed: () => overlayController
.onMorphActivitySelect(MorphSelection(token, morphFeature)),
tooltip: shouldDoActivity
? morphFeature.getDisplayCopy(context)
: getGrammarCopy(

View file

@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/analytics_details_popup/analytics_details_popup.dart';
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
@ -11,6 +9,7 @@ import 'package:fluffychat/pangea/learning_settings/constants/language_constants
import 'package:fluffychat/pangea/lemmas/construct_xp_widget.dart';
import 'package:fluffychat/pangea/lemmas/lemma_emoji_row.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
import 'package:fluffychat/pangea/toolbar/enums/message_mode_enum.dart';
import 'package:fluffychat/pangea/toolbar/widgets/message_selection_overlay.dart';
@ -19,6 +18,7 @@ import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_meaning_widget
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/lemma_widget.dart';
import 'package:fluffychat/pangea/toolbar/widgets/word_zoom/morphological_list_item.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
class WordZoomWidget extends StatelessWidget {
final PangeaToken token;
@ -38,6 +38,13 @@ class WordZoomWidget extends StatelessWidget {
void onEditDone() => overlayController.initializeTokensAndMode();
bool get hasEmojiActivity =>
overlayController.messageAnalyticsEntry?.hasActivity(
ActivityTypeEnum.emoji,
_selectedToken,
) ==
true;
@override
Widget build(BuildContext context) {
return ConstrainedBox(
@ -64,6 +71,7 @@ class WordZoomWidget extends StatelessWidget {
constraints: const BoxConstraints(
minHeight: 40,
),
color: Theme.of(context).colorScheme.surface,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
@ -114,21 +122,16 @@ class WordZoomWidget extends StatelessWidget {
alignment: Alignment.center,
child: LemmaEmojiRow(
cId: _selectedToken.vocabConstructID,
onTapOverride: () =>
overlayController.updateToolbarMode(
MessageMode.wordEmoji,
),
onTapOverride: hasEmojiActivity
? () => overlayController.updateToolbarMode(
MessageMode.wordEmoji,
)
: null,
isSelected: overlayController.toolbarMode ==
MessageMode.wordEmoji,
emojiSetCallback: () =>
overlayController.setState(() {}),
shouldShowEmojis: overlayController
.messageAnalyticsEntry
?.hasActivity(
MessageMode.wordEmoji.associatedActivityType!,
_selectedToken,
) ==
false,
shouldShowEmojis: !hasEmojiActivity,
),
),
],