1179 toolbar changes (#1209)
* updated toolbar buttons * initial work for toolbar updates * Add WordZoomWidget to display word and lemma information (#1214) --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/pangeachat/client/tree/1179-toolbar-changes?shareId=XXXX-XXXX-XXXX-XXXX). * word zoom card prototyped, activity generation in progress * adding copy for new construct uses * laying down TODOs * initial work for word zoom card * Always add part of speech to token's morph list * Prevent duplicate choices in lemma activity * Don't play token audio at start of morph activity * Only grant +1 points for emoji activity * Uncomment tryToSpeak function * Always update activity once complete * Added queuing / UI logic for morph activity buttons * code cleanup * added required data argument to logError calls * fix overflowing practice activity card and audio player on mobile --------- Co-authored-by: wcjord <32568597+wcjord@users.noreply.github.com>
This commit is contained in:
parent
0d477ad5b4
commit
1317989db0
52 changed files with 2598 additions and 1100 deletions
|
|
@ -4615,6 +4615,13 @@
|
|||
"constructUseCorHWLDesc": "Correct in hidden word activity",
|
||||
"constructUseIncHWLDesc": "Incorrect in hidden word activity",
|
||||
"constructUseIgnHWLDesc": "Ignored in hidden word activity",
|
||||
"constructUseCorLDesc": "Correct in lemma activity",
|
||||
"constructUseIncLDesc": "Incorrect in lemma activity",
|
||||
"constructUseIgnLDesc": "Ignored in lemma activity",
|
||||
"constructUseCorMDesc": "Correct in grammar activity",
|
||||
"constructUseIncMDesc": "Incorrect in grammar activity",
|
||||
"constructUseIgnMDesc": "Ignored in grammar activity",
|
||||
"constructUseEmojiDesc": "Correct in emoji activity",
|
||||
"constructUseNanDesc": "Not applicable",
|
||||
"xpIntoLevel": "{currentXP} / {maxXP} XP",
|
||||
"@xpIntoLevel": {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:animations/animations.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/pangea_reaction_picker.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
|
@ -19,13 +20,13 @@ import 'input_bar.dart';
|
|||
class ChatInputRow extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
// #Pangea
|
||||
final bool isOverlay;
|
||||
final MessageOverlayController? overlayController;
|
||||
// Pangea#
|
||||
|
||||
const ChatInputRow(
|
||||
this.controller, {
|
||||
// #Pangea
|
||||
this.isOverlay = false,
|
||||
this.overlayController,
|
||||
// Pangea#
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -68,7 +69,7 @@ class ChatInputRow extends StatelessWidget {
|
|||
CompositedTransformTarget(
|
||||
link: controller.choreographer.inputLayerLinkAndKey.link,
|
||||
child: Row(
|
||||
key: isOverlay
|
||||
key: overlayController != null
|
||||
? null
|
||||
: controller.choreographer.inputLayerLinkAndKey.key,
|
||||
// crossAxisAlignment: CrossAxisAlignment.end,
|
||||
|
|
@ -154,7 +155,7 @@ class ChatInputRow extends StatelessWidget {
|
|||
// Pangea#
|
||||
: const SizedBox.shrink(),
|
||||
// #Pangea
|
||||
PangeaReactionsPicker(controller),
|
||||
PangeaReactionsPicker(controller, overlayController),
|
||||
if (controller.selectedEvents.length == 1 &&
|
||||
!controller.selectedEvents.first
|
||||
.getDisplayEvent(controller.timeline!)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'dart:developer';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart';
|
||||
import 'package:fluffychat/utils/error_reporter.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
|
|
@ -27,6 +26,7 @@ class AudioPlayerWidget extends StatefulWidget {
|
|||
final PangeaAudioFile? matrixFile;
|
||||
final bool autoplay;
|
||||
final Function(bool)? setIsPlayingAudio;
|
||||
final double padding;
|
||||
// Pangea#
|
||||
|
||||
static String? currentId;
|
||||
|
|
@ -48,6 +48,7 @@ class AudioPlayerWidget extends StatefulWidget {
|
|||
this.sectionStartMS,
|
||||
this.sectionEndMS,
|
||||
this.setIsPlayingAudio,
|
||||
this.padding = 12.0,
|
||||
// Pangea#
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -354,15 +355,18 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
return Padding(
|
||||
// #Pangea
|
||||
// padding: const EdgeInsets.all(12.0),
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
padding: EdgeInsets.all(widget.padding),
|
||||
// Pangea#
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints:
|
||||
const BoxConstraints(maxWidth: FluffyThemes.columnWidth),
|
||||
// #Pangea
|
||||
// constraints:
|
||||
// const BoxConstraints(maxWidth: FluffyThemes.columnWidth),
|
||||
constraints: const BoxConstraints(maxWidth: 250),
|
||||
// Pangea#
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
|
|
@ -393,7 +397,9 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// #Pangea
|
||||
// const SizedBox(width: 8),
|
||||
// Pangea#
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
|
|
@ -410,9 +416,14 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
height: 32,
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 1,
|
||||
// #Pangea
|
||||
// margin: const EdgeInsets.symmetric(
|
||||
// horizontal: 1,
|
||||
// ),
|
||||
margin: const EdgeInsets.only(
|
||||
right: 0.5,
|
||||
),
|
||||
// Pangea#
|
||||
decoration: BoxDecoration(
|
||||
color: i < wavePosition
|
||||
? widget.color
|
||||
|
|
@ -453,7 +464,6 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
|
|||
),
|
||||
// #Pangea
|
||||
// const SizedBox(width: 8),
|
||||
const SizedBox(width: 5),
|
||||
// SizedBox(
|
||||
// width: 36,
|
||||
// child:
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ class ChoicesArray extends StatefulWidget {
|
|||
/// some uses of this widget want to disable clicking of the choices
|
||||
final bool isActive;
|
||||
|
||||
final String Function(String)? getDisplayCopy;
|
||||
|
||||
const ChoicesArray({
|
||||
super.key,
|
||||
required this.isLoading,
|
||||
|
|
@ -46,6 +48,7 @@ class ChoicesArray extends StatefulWidget {
|
|||
this.enableAudio = true,
|
||||
this.isActive = true,
|
||||
this.onLongPress,
|
||||
this.getDisplayCopy,
|
||||
this.id,
|
||||
});
|
||||
|
||||
|
|
@ -103,6 +106,7 @@ class ChoicesArrayState extends State<ChoicesArray> {
|
|||
disableInteraction: disableInteraction,
|
||||
isSelected: widget.selectedChoiceIndex == index,
|
||||
id: widget.id,
|
||||
getDisplayCopy: widget.getDisplayCopy,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
|
@ -134,6 +138,7 @@ class ChoiceItem extends StatelessWidget {
|
|||
required this.enableInteraction,
|
||||
required this.disableInteraction,
|
||||
required this.id,
|
||||
this.getDisplayCopy,
|
||||
});
|
||||
|
||||
final MapEntry<int, Choice> entry;
|
||||
|
|
@ -145,6 +150,7 @@ class ChoiceItem extends StatelessWidget {
|
|||
final VoidCallback enableInteraction;
|
||||
final VoidCallback disableInteraction;
|
||||
final String? id;
|
||||
final String Function(String)? getDisplayCopy;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -201,7 +207,9 @@ class ChoiceItem extends StatelessWidget {
|
|||
? null
|
||||
: () => onPressed(entry.value.text, entry.key),
|
||||
child: Text(
|
||||
entry.value.text,
|
||||
getDisplayCopy != null
|
||||
? getDisplayCopy!(entry.value.text)
|
||||
: entry.value.text,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -137,4 +137,5 @@ class ModelKey {
|
|||
static const String latestVersion = "latest_version";
|
||||
static const String latestBuildNumber = "latest_build_number";
|
||||
static const String mandatoryUpdate = "mandatory_update";
|
||||
static const String emoji = "emoji";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class PangeaEventTypes {
|
|||
// static const studentAnalyticsSummary = "pangea.usranalytics";
|
||||
static const summaryAnalytics = "pangea.summaryAnalytics";
|
||||
static const construct = "pangea.construct";
|
||||
static const userChosenEmoji = "p.emoji";
|
||||
|
||||
static const translation = "pangea.translation";
|
||||
static const tokens = "pangea.tokens";
|
||||
|
|
|
|||
|
|
@ -69,7 +69,9 @@ class MessageAnalyticsEntry {
|
|||
|
||||
late final bool _includeHiddenWordActivities;
|
||||
|
||||
late final List<TargetTokensAndActivityType> _activityQueue;
|
||||
final List<TargetTokensAndActivityType> _activityQueue = [];
|
||||
|
||||
final int _maxQueueLength = 3;
|
||||
|
||||
MessageAnalyticsEntry({
|
||||
required List<PangeaToken> tokens,
|
||||
|
|
@ -77,108 +79,77 @@ class MessageAnalyticsEntry {
|
|||
}) {
|
||||
_tokens = tokens;
|
||||
_includeHiddenWordActivities = includeHiddenWordActivities;
|
||||
_activityQueue = setActivityQueue();
|
||||
setActivityQueue();
|
||||
}
|
||||
|
||||
void _pushQueue(TargetTokensAndActivityType entry) {
|
||||
if (nextActivity?.activityType == ActivityTypeEnum.hiddenWordListening) {
|
||||
if (entry.activityType == ActivityTypeEnum.hiddenWordListening) {
|
||||
_activityQueue[0] = entry;
|
||||
} else {
|
||||
_activityQueue.insert(1, entry);
|
||||
}
|
||||
} else {
|
||||
_activityQueue.insert(0, entry);
|
||||
}
|
||||
|
||||
if (_activityQueue.length > _maxQueueLength) {
|
||||
_activityQueue.removeRange(
|
||||
_maxQueueLength,
|
||||
_activityQueue.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _popQueue() {
|
||||
if (hasHiddenWordActivity) {
|
||||
_activityQueue.removeAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
void _filterQueue(ActivityTypeEnum activityType) {
|
||||
_activityQueue.removeWhere((a) => a.activityType == activityType);
|
||||
}
|
||||
|
||||
void _clearQueue() {
|
||||
_activityQueue.clear();
|
||||
}
|
||||
|
||||
TargetTokensAndActivityType? get nextActivity =>
|
||||
_activityQueue.isNotEmpty ? _activityQueue.first : null;
|
||||
|
||||
/// 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;
|
||||
bool get hasHiddenWordActivity =>
|
||||
nextActivity?.activityType.hiddenType ?? false;
|
||||
|
||||
int get numActivities => _activityQueue.length;
|
||||
|
||||
// /// 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
|
||||
List<TargetTokensAndActivityType> setActivityQueue() {
|
||||
void setActivityQueue() {
|
||||
final List<TargetTokensAndActivityType> queue = [];
|
||||
|
||||
// for each token in the message
|
||||
// pick a random activity type from the eligible types
|
||||
for (final token in _tokens) {
|
||||
// get all the eligible activity types for the token
|
||||
// based on the context of the message
|
||||
final eligibleTypesBasedOnContext = token.eligibleActivityTypes
|
||||
// we want to filter hidden word types from this part of the process
|
||||
.where((type) => type != ActivityTypeEnum.hiddenWordListening)
|
||||
// there have to be at least 4 tokens in the message that can be heard for word focus listening
|
||||
.where(
|
||||
(type) =>
|
||||
canDoWordFocusListening ||
|
||||
type != ActivityTypeEnum.wordFocusListening,
|
||||
)
|
||||
.toList();
|
||||
|
||||
// if there are no eligible types, continue to the next token
|
||||
if (eligibleTypesBasedOnContext.isEmpty) continue;
|
||||
|
||||
// chose a random activity type from the eligible types for that token
|
||||
queue.add(
|
||||
TargetTokensAndActivityType(
|
||||
tokens: [token],
|
||||
activityType: eligibleTypesBasedOnContext[
|
||||
Random().nextInt(eligibleTypesBasedOnContext.length)],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// sort the queue by the total xp of the tokens, lowest first
|
||||
queue.sort(
|
||||
(a, b) => a.tokens
|
||||
.map((t) => t.vocabConstruct.points)
|
||||
.reduce((a, b) => a + b)
|
||||
.compareTo(
|
||||
b.tokens
|
||||
.map((t) => t.vocabConstruct.points)
|
||||
.reduce((a, b) => a + b),
|
||||
),
|
||||
);
|
||||
|
||||
// if applicable, add a hidden word activity to the front of the queue
|
||||
final hiddenWordActivity = getHiddenWordActivity(queue.length);
|
||||
if (hiddenWordActivity != null) {
|
||||
queue.insert(0, hiddenWordActivity);
|
||||
_pushQueue(hiddenWordActivity);
|
||||
}
|
||||
|
||||
// limit to 3 activities
|
||||
final limited = queue.take(3).toList();
|
||||
|
||||
// debugPrint("activities for ${PangeaToken.reconstructText(_tokens)}");
|
||||
// for (final activity in limited) {
|
||||
// debugPrint("activity: ${activity.activityType}");
|
||||
// for (final token in activity.tokens) {
|
||||
// debugPrint("token: ${token.analyticsDebugPrint}");
|
||||
// }
|
||||
// }
|
||||
|
||||
return limited;
|
||||
}
|
||||
|
||||
/// Adds a word focus listening activity to the front of the queue
|
||||
/// And limits to 3 activities
|
||||
void addForWordMeaning(PangeaToken selectedToken) {
|
||||
final int index = _activityQueue.isNotEmpty &&
|
||||
_activityQueue.first.activityType ==
|
||||
ActivityTypeEnum.hiddenWordListening
|
||||
? 1
|
||||
: 0;
|
||||
|
||||
_activityQueue.insert(
|
||||
index,
|
||||
TargetTokensAndActivityType(
|
||||
tokens: [selectedToken],
|
||||
activityType: ActivityTypeEnum.wordMeaning,
|
||||
),
|
||||
/// And limits to _maxQueueLength activities
|
||||
void addTokenToActivityQueue(
|
||||
PangeaToken token, {
|
||||
ActivityTypeEnum type = ActivityTypeEnum.wordMeaning,
|
||||
}) {
|
||||
final entry = TargetTokensAndActivityType(
|
||||
tokens: [token],
|
||||
activityType: ActivityTypeEnum.wordMeaning,
|
||||
);
|
||||
// remove down to three activities
|
||||
if (_activityQueue.length > 3) {
|
||||
_activityQueue.removeRange(3, _activityQueue.length);
|
||||
}
|
||||
}
|
||||
|
||||
int get numActivities => _activityQueue.length;
|
||||
|
||||
void clearActivityQueue() {
|
||||
_activityQueue.clear();
|
||||
_pushQueue(entry);
|
||||
}
|
||||
|
||||
/// Returns a hidden word activity if there is a sequence of tokens that have hiddenWordListening in their eligibleActivityTypes
|
||||
|
|
@ -190,7 +161,7 @@ class MessageAnalyticsEntry {
|
|||
|
||||
// we will only do hidden word listening 50% of the time
|
||||
// if there are no other activities to do, we will always do hidden word listening
|
||||
if (numOtherActivities >= 3 && Random().nextDouble() < 0.5) {
|
||||
if (numOtherActivities >= _maxQueueLength && Random().nextDouble() < 0.5) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -198,8 +169,11 @@ class MessageAnalyticsEntry {
|
|||
final List<List<PangeaToken>> sequences = [];
|
||||
List<PangeaToken> currentSequence = [];
|
||||
for (final token in _tokens) {
|
||||
if (token.eligibleActivityTypes
|
||||
.contains(ActivityTypeEnum.hiddenWordListening)) {
|
||||
if (token.shouldDoActivity(
|
||||
a: ActivityTypeEnum.hiddenWordListening,
|
||||
feature: null,
|
||||
tag: null,
|
||||
)) {
|
||||
currentSequence.add(token);
|
||||
} else {
|
||||
if (currentSequence.isNotEmpty) {
|
||||
|
|
@ -226,15 +200,13 @@ class MessageAnalyticsEntry {
|
|||
);
|
||||
}
|
||||
|
||||
void onActivityComplete(PracticeActivityModel completed) {
|
||||
_activityQueue.removeWhere(
|
||||
(a) => a.matchesActivity(completed),
|
||||
);
|
||||
void onActivityComplete() {
|
||||
_popQueue();
|
||||
}
|
||||
|
||||
void revealAllTokens() {
|
||||
_activityQueue.removeWhere((a) => a.activityType.hiddenType);
|
||||
}
|
||||
void exitPracticeFlow() => _clearQueue();
|
||||
|
||||
void revealAllTokens() => _filterQueue(ActivityTypeEnum.hiddenWordListening);
|
||||
|
||||
bool isTokenInHiddenWordActivity(PangeaToken token) => _activityQueue.any(
|
||||
(activity) =>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import 'package:fluffychat/pangea/controllers/language_detection_controller.dart
|
|||
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/message_data_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/permissions_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/practice_activity_generation_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/practice_activity_record_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/speech_to_text_controller.dart';
|
||||
|
|
@ -57,7 +56,6 @@ class PangeaController {
|
|||
late SpeechToTextController speechToText;
|
||||
late LanguageDetectionController languageDetection;
|
||||
late PracticeActivityRecordController activityRecordController;
|
||||
late PracticeGenerationController practiceGenerationController;
|
||||
|
||||
///store Services
|
||||
late PStore pStoreService;
|
||||
|
|
@ -114,7 +112,6 @@ class PangeaController {
|
|||
speechToText = SpeechToTextController(this);
|
||||
languageDetection = LanguageDetectionController(this);
|
||||
activityRecordController = PracticeActivityRecordController(this);
|
||||
practiceGenerationController = PracticeGenerationController(this);
|
||||
PAuthGaurd.pController = this;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'dart:typed_data';
|
|||
import 'package:fluffychat/pangea/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/network/urls.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
enum ActivityTypeEnum { wordMeaning, wordFocusListening, hiddenWordListening }
|
||||
enum ActivityTypeEnum {
|
||||
wordMeaning,
|
||||
wordFocusListening,
|
||||
hiddenWordListening,
|
||||
lemmaId,
|
||||
emoji,
|
||||
morphId
|
||||
}
|
||||
|
||||
extension ActivityTypeExtension on ActivityTypeEnum {
|
||||
String get string {
|
||||
|
|
@ -13,6 +22,12 @@ extension ActivityTypeExtension on ActivityTypeEnum {
|
|||
return 'word_focus_listening';
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return 'hidden_word_listening';
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
return 'lemma_id';
|
||||
case ActivityTypeEnum.emoji:
|
||||
return 'emoji';
|
||||
case ActivityTypeEnum.morphId:
|
||||
return 'morph_id';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -20,6 +35,9 @@ extension ActivityTypeExtension on ActivityTypeEnum {
|
|||
switch (this) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
case ActivityTypeEnum.emoji:
|
||||
case ActivityTypeEnum.morphId:
|
||||
return false;
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return true;
|
||||
|
|
@ -29,6 +47,9 @@ extension ActivityTypeExtension on ActivityTypeEnum {
|
|||
bool get includeTTSOnClick {
|
||||
switch (this) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
case ActivityTypeEnum.emoji:
|
||||
case ActivityTypeEnum.morphId:
|
||||
return false;
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
|
|
@ -53,6 +74,12 @@ extension ActivityTypeExtension on ActivityTypeEnum {
|
|||
case 'hidden_word_listening':
|
||||
case 'hiddenWordListening':
|
||||
return ActivityTypeEnum.hiddenWordListening;
|
||||
case 'lemma_id':
|
||||
return ActivityTypeEnum.lemmaId;
|
||||
case 'emoji':
|
||||
return ActivityTypeEnum.emoji;
|
||||
case 'morph_id':
|
||||
return ActivityTypeEnum.morphId;
|
||||
default:
|
||||
throw Exception('Unknown activity type: $split');
|
||||
}
|
||||
|
|
@ -78,6 +105,37 @@ extension ActivityTypeExtension on ActivityTypeEnum {
|
|||
ConstructUseTypeEnum.incHWL,
|
||||
ConstructUseTypeEnum.ignHWL,
|
||||
];
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
return [
|
||||
ConstructUseTypeEnum.corL,
|
||||
ConstructUseTypeEnum.incL,
|
||||
ConstructUseTypeEnum.ignL,
|
||||
];
|
||||
case ActivityTypeEnum.emoji:
|
||||
return [ConstructUseTypeEnum.em];
|
||||
case ActivityTypeEnum.morphId:
|
||||
return [
|
||||
ConstructUseTypeEnum.corM,
|
||||
ConstructUseTypeEnum.incM,
|
||||
ConstructUseTypeEnum.ignM,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
ConstructUseTypeEnum get correctUse {
|
||||
switch (this) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return ConstructUseTypeEnum.corPA;
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return ConstructUseTypeEnum.corWL;
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return ConstructUseTypeEnum.corHWL;
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
return ConstructUseTypeEnum.corL;
|
||||
case ActivityTypeEnum.emoji:
|
||||
return ConstructUseTypeEnum.em;
|
||||
case ActivityTypeEnum.morphId:
|
||||
return ConstructUseTypeEnum.corM;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +145,27 @@ extension ActivityTypeExtension on ActivityTypeEnum {
|
|||
case ActivityTypeEnum.wordMeaning:
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
case ActivityTypeEnum.emoji:
|
||||
return (id) => id.type == ConstructTypeEnum.vocab;
|
||||
case ActivityTypeEnum.morphId:
|
||||
return (id) => id.type == ConstructTypeEnum.morph;
|
||||
}
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return Icons.translate;
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return Icons.hearing;
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
return Symbols.dictionary;
|
||||
case ActivityTypeEnum.emoji:
|
||||
return Icons.emoji_emotions;
|
||||
case ActivityTypeEnum.morphId:
|
||||
return Icons.format_shapes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
|
|
@ -12,52 +16,46 @@ enum ConstructUseTypeEnum {
|
|||
/// produced in chat by user and igc was not run
|
||||
unk,
|
||||
|
||||
/// selected correctly in IT flow
|
||||
/// interactive translation activity
|
||||
corIt,
|
||||
|
||||
/// encountered as IT distractor and correctly ignored it
|
||||
ignIt,
|
||||
|
||||
/// encountered as it distractor and selected it
|
||||
incIt,
|
||||
|
||||
/// encountered in igc match and ignored match
|
||||
/// interactive grammar checking activity
|
||||
corIGC,
|
||||
incIGC,
|
||||
ignIGC,
|
||||
|
||||
/// selected correctly in IGC flow
|
||||
corIGC,
|
||||
|
||||
/// encountered as distractor in IGC flow and selected it
|
||||
incIGC,
|
||||
|
||||
/// selected correctly in word meaning in context practice activity
|
||||
/// word meaning in context practice activity
|
||||
corPA,
|
||||
|
||||
/// encountered as distractor in word meaning in context practice activity and correctly ignored it
|
||||
/// Currently not used
|
||||
ignPA,
|
||||
|
||||
/// was target construct in word meaning in context practice activity and incorrectly selected
|
||||
incPA,
|
||||
|
||||
/// was target lemma in word-focus listening activity and correctly selected
|
||||
/// applies to target lemma in word-focus listening activity
|
||||
corWL,
|
||||
|
||||
/// a form of lemma was read-aloud in word-focus listening activity and incorrectly selected
|
||||
incWL,
|
||||
|
||||
/// a form of the lemma was read-aloud in word-focus listening activity and correctly ignored
|
||||
ignWL,
|
||||
|
||||
/// correctly chose a form of the lemma in a hidden word listening activity
|
||||
/// applies to the form of the lemma in a hidden word listening activity
|
||||
corHWL,
|
||||
|
||||
/// incorrectly chose a form of the lemma in a hidden word listening activity
|
||||
incHWL,
|
||||
|
||||
/// ignored a form of the lemma in a hidden word listening activity
|
||||
ignHWL,
|
||||
|
||||
/// lemma id activity
|
||||
corL,
|
||||
incL,
|
||||
ignL,
|
||||
|
||||
/// morph id activity
|
||||
corM,
|
||||
incM,
|
||||
ignM,
|
||||
|
||||
/// emoji activity
|
||||
/// No correct/incorrect/ignored distinction is made
|
||||
/// User can select any emoji
|
||||
em,
|
||||
|
||||
/// not defined, likely a new construct introduced by choreo and not yet classified by an old version of the client
|
||||
nan
|
||||
}
|
||||
|
|
@ -103,52 +101,60 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
return L10n.of(context).constructUseIncHWLDesc;
|
||||
case ConstructUseTypeEnum.ignHWL:
|
||||
return L10n.of(context).constructUseIgnHWLDesc;
|
||||
case ConstructUseTypeEnum.corL:
|
||||
return L10n.of(context).constructUseCorLDesc;
|
||||
case ConstructUseTypeEnum.incL:
|
||||
return L10n.of(context).constructUseIncLDesc;
|
||||
case ConstructUseTypeEnum.ignL:
|
||||
return L10n.of(context).constructUseIgnLDesc;
|
||||
case ConstructUseTypeEnum.corM:
|
||||
return L10n.of(context).constructUseCorMDesc;
|
||||
case ConstructUseTypeEnum.incM:
|
||||
return L10n.of(context).constructUseIncMDesc;
|
||||
case ConstructUseTypeEnum.ignM:
|
||||
return L10n.of(context).constructUseIgnMDesc;
|
||||
case ConstructUseTypeEnum.em:
|
||||
return L10n.of(context).constructUseEmojiDesc;
|
||||
case ConstructUseTypeEnum.nan:
|
||||
return L10n.of(context).constructUseNanDesc;
|
||||
}
|
||||
}
|
||||
|
||||
ActivityTypeEnum get activityType => ActivityTypeEnum.values.firstWhere(
|
||||
(e) => e.associatedUseTypes.contains(this),
|
||||
orElse: () {
|
||||
debugger(when: kDebugMode);
|
||||
return ActivityTypeEnum.wordMeaning;
|
||||
},
|
||||
);
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
// all minus for wrong answer
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
case ConstructUseTypeEnum.incWL:
|
||||
case ConstructUseTypeEnum.incHWL:
|
||||
return Icons.dangerous_outlined;
|
||||
|
||||
// correct in word meaning
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
return Icons.add_task_outlined;
|
||||
|
||||
// correct in audio practice
|
||||
case ConstructUseTypeEnum.corWL:
|
||||
case ConstructUseTypeEnum.corHWL:
|
||||
return Icons.volume_up_outlined;
|
||||
|
||||
// correct in translation
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
return Icons.translate_outlined;
|
||||
|
||||
// written correctly without help
|
||||
case ConstructUseTypeEnum.wa:
|
||||
return Icons.thumb_up_outlined;
|
||||
|
||||
// correct in grammar correction
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
return Icons.spellcheck_outlined;
|
||||
|
||||
// ignored
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
case ConstructUseTypeEnum.ignIGC:
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
case ConstructUseTypeEnum.ignPA:
|
||||
case ConstructUseTypeEnum.ignWL:
|
||||
case ConstructUseTypeEnum.incWL:
|
||||
case ConstructUseTypeEnum.incHWL:
|
||||
case ConstructUseTypeEnum.ignHWL:
|
||||
return Icons.block_outlined;
|
||||
|
||||
case ConstructUseTypeEnum.ga:
|
||||
return Icons.edit_outlined;
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
case ConstructUseTypeEnum.corWL:
|
||||
case ConstructUseTypeEnum.corHWL:
|
||||
case ConstructUseTypeEnum.corL:
|
||||
case ConstructUseTypeEnum.incL:
|
||||
case ConstructUseTypeEnum.ignL:
|
||||
case ConstructUseTypeEnum.corM:
|
||||
case ConstructUseTypeEnum.incM:
|
||||
case ConstructUseTypeEnum.ignM:
|
||||
case ConstructUseTypeEnum.em:
|
||||
return activityType.icon;
|
||||
|
||||
case ConstructUseTypeEnum.unk:
|
||||
case ConstructUseTypeEnum.nan:
|
||||
|
|
@ -173,9 +179,12 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
return 3;
|
||||
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
case ConstructUseTypeEnum.corL:
|
||||
case ConstructUseTypeEnum.corM:
|
||||
return 2;
|
||||
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
case ConstructUseTypeEnum.em:
|
||||
return 1;
|
||||
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
|
|
@ -183,6 +192,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.ignPA:
|
||||
case ConstructUseTypeEnum.ignWL:
|
||||
case ConstructUseTypeEnum.ignHWL:
|
||||
case ConstructUseTypeEnum.ignL:
|
||||
case ConstructUseTypeEnum.ignM:
|
||||
case ConstructUseTypeEnum.unk:
|
||||
case ConstructUseTypeEnum.nan:
|
||||
return 0;
|
||||
|
|
@ -192,6 +203,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
case ConstructUseTypeEnum.incL:
|
||||
case ConstructUseTypeEnum.incM:
|
||||
return -2;
|
||||
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ import 'package:matrix/matrix.dart';
|
|||
enum MessageMode {
|
||||
practiceActivity,
|
||||
textToSpeech,
|
||||
definition,
|
||||
translation,
|
||||
speechToText,
|
||||
wordZoom,
|
||||
noneSelected,
|
||||
}
|
||||
|
||||
extension MessageModeExtension on MessageMode {
|
||||
|
|
@ -20,13 +21,12 @@ extension MessageModeExtension on MessageMode {
|
|||
return Symbols.text_to_speech;
|
||||
case MessageMode.speechToText:
|
||||
return Symbols.speech_to_text;
|
||||
//TODO change icon for audio messages
|
||||
case MessageMode.definition:
|
||||
return Icons.book;
|
||||
case MessageMode.practiceActivity:
|
||||
return Symbols.fitness_center;
|
||||
default:
|
||||
return Icons.error; // Icon to indicate an error or unsupported mode
|
||||
case MessageMode.wordZoom:
|
||||
return Symbols.dictionary;
|
||||
case MessageMode.noneSelected:
|
||||
return Icons.error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -38,13 +38,12 @@ extension MessageModeExtension on MessageMode {
|
|||
return L10n.of(context).messageAudio;
|
||||
case MessageMode.speechToText:
|
||||
return L10n.of(context).speechToTextTooltip;
|
||||
case MessageMode.definition:
|
||||
return L10n.of(context).definitions;
|
||||
case MessageMode.practiceActivity:
|
||||
return L10n.of(context).practice;
|
||||
default:
|
||||
return L10n.of(context)
|
||||
.oopsSomethingWentWrong; // Title to indicate an error or unsupported mode
|
||||
case MessageMode.wordZoom:
|
||||
return L10n.of(context).vocab;
|
||||
case MessageMode.noneSelected:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,28 +55,27 @@ extension MessageModeExtension on MessageMode {
|
|||
return L10n.of(context).audioTooltip;
|
||||
case MessageMode.speechToText:
|
||||
return L10n.of(context).speechToTextTooltip;
|
||||
case MessageMode.definition:
|
||||
return L10n.of(context).define;
|
||||
case MessageMode.practiceActivity:
|
||||
return L10n.of(context).practice;
|
||||
default:
|
||||
return L10n.of(context)
|
||||
.oopsSomethingWentWrong; // Title to indicate an error or unsupported mode
|
||||
case MessageMode.wordZoom:
|
||||
return L10n.of(context).vocab;
|
||||
case MessageMode.noneSelected:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
bool shouldShowAsToolbarButton(Event event) {
|
||||
switch (this) {
|
||||
case MessageMode.translation:
|
||||
return event.messageType == MessageTypes.Text;
|
||||
case MessageMode.textToSpeech:
|
||||
return event.messageType == MessageTypes.Text;
|
||||
case MessageMode.definition:
|
||||
return event.messageType == MessageTypes.Text;
|
||||
case MessageMode.speechToText:
|
||||
return event.messageType == MessageTypes.Audio;
|
||||
case MessageMode.practiceActivity:
|
||||
return true;
|
||||
case MessageMode.wordZoom:
|
||||
case MessageMode.noneSelected:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +86,8 @@ extension MessageModeExtension on MessageMode {
|
|||
) =>
|
||||
numActivitiesCompleted >= index || totallyDone;
|
||||
|
||||
bool get showButton => this != MessageMode.practiceActivity;
|
||||
|
||||
Color iconButtonColor(
|
||||
BuildContext context,
|
||||
int index,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import 'package:collection/collection.dart';
|
|||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// A wrapper around a list of [OneConstructUse]s, used to simplify
|
||||
/// the process of filtering / sorting / displaying the events.
|
||||
|
|
@ -219,4 +221,91 @@ class ConstructListModel {
|
|||
}).where((entry) => entry.value.isNotEmpty),
|
||||
);
|
||||
}
|
||||
|
||||
List<String> morphActivityDistractors(
|
||||
String morphFeature,
|
||||
String morphTag,
|
||||
) {
|
||||
final List<ConstructUses> morphConstructs = constructList(
|
||||
type: ConstructTypeEnum.morph,
|
||||
);
|
||||
|
||||
final List<String> possibleDistractors = morphConstructs
|
||||
.where(
|
||||
(c) =>
|
||||
c.category == morphFeature.toLowerCase() &&
|
||||
c.lemma.toLowerCase() != morphTag.toLowerCase(),
|
||||
)
|
||||
.map((c) => c.lemma)
|
||||
.toList();
|
||||
|
||||
possibleDistractors.shuffle();
|
||||
return possibleDistractors.take(3).toList();
|
||||
}
|
||||
|
||||
Future<List<String>> lemmaActivityDistractors(PangeaToken token) async {
|
||||
final List<String> lemmas = constructList(type: ConstructTypeEnum.vocab)
|
||||
.map((c) => c.lemma)
|
||||
.toSet()
|
||||
.toList();
|
||||
|
||||
// Offload computation to an isolate
|
||||
final Map<String, int> distances =
|
||||
await compute(_computeDistancesInIsolate, {
|
||||
'lemmas': lemmas,
|
||||
'target': token.lemma.text,
|
||||
});
|
||||
|
||||
// Sort lemmas by distance
|
||||
final sortedLemmas = distances.keys.toList()
|
||||
..sort((a, b) => distances[a]!.compareTo(distances[b]!));
|
||||
|
||||
// Take the shortest 4
|
||||
final choices = sortedLemmas.take(4).toList();
|
||||
if (!choices.contains(token.lemma.text)) {
|
||||
final random = Random();
|
||||
choices[random.nextInt(4)] = token.lemma.text;
|
||||
}
|
||||
return choices;
|
||||
}
|
||||
|
||||
// isolate helper function
|
||||
Map<String, int> _computeDistancesInIsolate(Map<String, dynamic> params) {
|
||||
final List<String> lemmas = params['lemmas'];
|
||||
final String target = params['target'];
|
||||
|
||||
// Calculate Levenshtein distances
|
||||
final Map<String, int> distances = {};
|
||||
for (final lemma in lemmas) {
|
||||
distances[lemma] = levenshteinDistanceSync(target, lemma);
|
||||
}
|
||||
return distances;
|
||||
}
|
||||
|
||||
int levenshteinDistanceSync(String s, String t) {
|
||||
final int m = s.length;
|
||||
final int n = t.length;
|
||||
final List<List<int>> dp = List.generate(
|
||||
m + 1,
|
||||
(_) => List.generate(n + 1, (_) => 0),
|
||||
);
|
||||
|
||||
for (int i = 0; i <= m; i++) {
|
||||
for (int j = 0; j <= n; j++) {
|
||||
if (i == 0) {
|
||||
dp[i][j] = j;
|
||||
} else if (j == 0) {
|
||||
dp[i][j] = i;
|
||||
} else if (s[i - 1] == t[j - 1]) {
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
dp[i][j] = 1 +
|
||||
[dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]]
|
||||
.reduce((a, b) => a < b ? a : b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dp[m][n];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../constants/model_keys.dart';
|
||||
import 'lemma.dart';
|
||||
|
|
@ -24,16 +31,16 @@ class PangeaToken {
|
|||
/// https://universaldependencies.org/u/pos/
|
||||
final String pos;
|
||||
|
||||
/// [morph] ex {} - morphological features of the token
|
||||
/// [_morph] ex {} - morphological features of the token
|
||||
/// https://universaldependencies.org/u/feat/
|
||||
final Map<String, dynamic> morph;
|
||||
final Map<String, dynamic> _morph;
|
||||
|
||||
PangeaToken({
|
||||
required this.text,
|
||||
required this.lemma,
|
||||
required this.pos,
|
||||
required this.morph,
|
||||
});
|
||||
required Map<String, dynamic> morph,
|
||||
}) : _morph = morph;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
|
|
@ -47,6 +54,15 @@ class PangeaToken {
|
|||
@override
|
||||
int get hashCode => text.content.hashCode ^ text.offset.hashCode;
|
||||
|
||||
Map<String, dynamic> get morph {
|
||||
if (_morph.keys.map((key) => key.toLowerCase()).contains("pos")) {
|
||||
return _morph;
|
||||
}
|
||||
final morphWithPos = Map<String, dynamic>.from(_morph);
|
||||
morphWithPos["pos"] = pos;
|
||||
return morphWithPos;
|
||||
}
|
||||
|
||||
/// reconstructs the text from the tokens
|
||||
/// [tokens] - the tokens to reconstruct
|
||||
/// [debugWalkThrough] - if true, will start the debugger
|
||||
|
|
@ -196,62 +212,91 @@ class PangeaToken {
|
|||
switch (a) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return canBeDefined;
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
return lemma.saveVocab;
|
||||
case ActivityTypeEnum.emoji:
|
||||
return true;
|
||||
case ActivityTypeEnum.morphId:
|
||||
return morph.isNotEmpty;
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return false;
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return canBeHeard;
|
||||
}
|
||||
}
|
||||
|
||||
bool _didActivity(ActivityTypeEnum a) {
|
||||
bool _didActivity(
|
||||
ActivityTypeEnum a, [
|
||||
String? morphFeature,
|
||||
String? morphTag,
|
||||
]) {
|
||||
if ((morphFeature == null || morphTag == null) &&
|
||||
a == ActivityTypeEnum.morphId) {
|
||||
debugger(when: kDebugMode);
|
||||
return true;
|
||||
}
|
||||
switch (a) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return vocabConstruct.uses
|
||||
.map((u) => u.useType)
|
||||
.any((u) => a.associatedUseTypes.contains(u));
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return vocabConstruct.uses
|
||||
// TODO - double-check that form is going to be available here
|
||||
// .where((u) =>
|
||||
// u.form?.toLowerCase() == text.content.toLowerCase(),)
|
||||
.map((u) => u.useType)
|
||||
.any((u) => a.associatedUseTypes.contains(u));
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
case ActivityTypeEnum.emoji:
|
||||
return vocabConstruct.uses
|
||||
// TODO - double-check that form is going to be available here
|
||||
// .where((u) =>
|
||||
// u.form?.toLowerCase() == text.content.toLowerCase(),)
|
||||
.map((u) => u.useType)
|
||||
.any((u) => a.associatedUseTypes.contains(u));
|
||||
case ActivityTypeEnum.morphId:
|
||||
return morph.entries
|
||||
.map((e) => morphConstruct(morphFeature!, morphTag!).uses)
|
||||
.expand((e) => e)
|
||||
.any(
|
||||
(u) =>
|
||||
a.associatedUseTypes.contains(u.useType) &&
|
||||
u.form == text.content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _didActivitySuccessfully(ActivityTypeEnum a) {
|
||||
bool _didActivitySuccessfully(
|
||||
ActivityTypeEnum a, [
|
||||
String? morphFeature,
|
||||
String? morphTag,
|
||||
]) {
|
||||
if ((morphFeature == null || morphTag == null) &&
|
||||
a == ActivityTypeEnum.morphId) {
|
||||
debugger(when: kDebugMode);
|
||||
return true;
|
||||
}
|
||||
switch (a) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return vocabConstruct.uses
|
||||
.map((u) => u.useType)
|
||||
.any((u) => u == ConstructUseTypeEnum.corPA);
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return vocabConstruct.uses
|
||||
// TODO - double-check that form is going to be available here
|
||||
// .where((u) =>
|
||||
// u.form?.toLowerCase() == text.content.toLowerCase(),)
|
||||
.map((u) => u.useType)
|
||||
.any((u) => u == ConstructUseTypeEnum.corWL);
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
case ActivityTypeEnum.emoji:
|
||||
return vocabConstruct.uses
|
||||
// TODO - double-check that form is going to be available here
|
||||
// .where((u) =>
|
||||
// u.form?.toLowerCase() == text.content.toLowerCase(),)
|
||||
.map((u) => u.useType)
|
||||
.any((u) => u == ConstructUseTypeEnum.corHWL);
|
||||
.any((u) => u == a.correctUse);
|
||||
// Note that it matters less if they did morphId in general, than if they did it with the particular feature
|
||||
case ActivityTypeEnum.morphId:
|
||||
if (morphFeature == null || morphTag == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return false;
|
||||
}
|
||||
return morphConstruct(morphFeature, morphTag)
|
||||
.uses
|
||||
.any((u) => u.useType == a.correctUse && u.form == text.content);
|
||||
}
|
||||
}
|
||||
|
||||
bool _isActivityProbablyLevelAppropriate(ActivityTypeEnum a) {
|
||||
bool _isActivityProbablyLevelAppropriate(
|
||||
ActivityTypeEnum a, [
|
||||
String? morphFeature,
|
||||
String? morphTag,
|
||||
]) {
|
||||
switch (a) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
if (daysSinceLastUseByType(ActivityTypeEnum.wordMeaning) < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isContentWord) {
|
||||
return vocabConstruct.points < 30;
|
||||
} else if (canBeDefined) {
|
||||
|
|
@ -262,22 +307,83 @@ class PangeaToken {
|
|||
case ActivityTypeEnum.wordFocusListening:
|
||||
return !_didActivitySuccessfully(a) || daysSinceLastUseByType(a) > 30;
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return daysSinceLastUseByType(a) > 2;
|
||||
return daysSinceLastUseByType(a) > 7;
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
return daysSinceLastUseByType(a) > 7;
|
||||
case ActivityTypeEnum.emoji:
|
||||
return getEmoji() == null;
|
||||
case ActivityTypeEnum.morphId:
|
||||
if (morphFeature == null || morphTag == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return false;
|
||||
}
|
||||
return daysSinceLastUseMorph(morphFeature, morphTag) > 1 &&
|
||||
morphConstruct(morphFeature, morphTag).points < 5;
|
||||
}
|
||||
}
|
||||
|
||||
bool get shouldDoPosActivity => shouldDoMorphActivity("Pos");
|
||||
|
||||
bool shouldDoMorphActivity(String feature) {
|
||||
return shouldDoActivity(
|
||||
a: ActivityTypeEnum.morphId,
|
||||
feature: feature,
|
||||
tag: getMorphTag(feature),
|
||||
);
|
||||
}
|
||||
|
||||
/// Safely get morph tag for a given feature without regard for case
|
||||
String? getMorphTag(String feature) {
|
||||
if (morph.containsKey(feature)) return morph[feature];
|
||||
if (morph.containsKey(feature.toLowerCase())) {
|
||||
return morph[feature.toLowerCase()];
|
||||
}
|
||||
final lowerCaseEntries = morph.entries.map(
|
||||
(e) => MapEntry(e.key.toLowerCase(), e.value),
|
||||
);
|
||||
return lowerCaseEntries
|
||||
.firstWhereOrNull(
|
||||
(e) => e.key == feature.toLowerCase(),
|
||||
)
|
||||
?.value;
|
||||
}
|
||||
|
||||
Future<bool> canGenerateDistractors(
|
||||
ActivityTypeEnum type, {
|
||||
String? morphFeature,
|
||||
String? morphTag,
|
||||
}) async {
|
||||
final constructListModel =
|
||||
MatrixState.pangeaController.getAnalytics.constructListModel;
|
||||
switch (type) {
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
final distractors =
|
||||
await constructListModel.lemmaActivityDistractors(this);
|
||||
return distractors.isNotEmpty;
|
||||
case ActivityTypeEnum.morphId:
|
||||
final distractors = constructListModel.morphActivityDistractors(
|
||||
morphFeature!,
|
||||
morphTag!,
|
||||
);
|
||||
return distractors.isNotEmpty;
|
||||
case ActivityTypeEnum.emoji:
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// maybe for every 5 points of xp for a particular activity, increment the days between uses by 2
|
||||
bool shouldDoActivity(ActivityTypeEnum a) =>
|
||||
lemma.saveVocab &&
|
||||
_isActivityBasicallyEligible(a) &&
|
||||
_isActivityProbablyLevelAppropriate(a);
|
||||
|
||||
/// we try to guess if the user is click on a token specifically or clicking on a message in general
|
||||
/// if we think the word might be new for the learner, then we'll assume they're clicking on the word
|
||||
bool get shouldDoWordMeaningOnClick =>
|
||||
lemma.saveVocab &&
|
||||
canBeDefined &&
|
||||
daysSinceLastUseByType(ActivityTypeEnum.wordMeaning) > 1;
|
||||
bool shouldDoActivity({
|
||||
required ActivityTypeEnum a,
|
||||
required String? feature,
|
||||
required String? tag,
|
||||
}) {
|
||||
return lemma.saveVocab &&
|
||||
_isActivityBasicallyEligible(a) &&
|
||||
_isActivityProbablyLevelAppropriate(a, feature, tag);
|
||||
}
|
||||
|
||||
List<ActivityTypeEnum> get eligibleActivityTypes {
|
||||
final List<ActivityTypeEnum> eligibleActivityTypes = [];
|
||||
|
|
@ -287,7 +393,7 @@ class PangeaToken {
|
|||
}
|
||||
|
||||
for (final type in ActivityTypeEnum.values) {
|
||||
if (shouldDoActivity(type)) {
|
||||
if (shouldDoActivity(a: type, feature: null, tag: null)) {
|
||||
eligibleActivityTypes.add(type);
|
||||
}
|
||||
}
|
||||
|
|
@ -295,20 +401,37 @@ class PangeaToken {
|
|||
return eligibleActivityTypes;
|
||||
}
|
||||
|
||||
ConstructUses get vocabConstruct {
|
||||
final vocab = constructs.firstWhereOrNull(
|
||||
(element) => element.id.type == ConstructTypeEnum.vocab,
|
||||
);
|
||||
if (vocab == null) {
|
||||
return ConstructUses(
|
||||
ConstructUses get vocabConstruct =>
|
||||
MatrixState.pangeaController.getAnalytics.constructListModel
|
||||
.getConstructUses(
|
||||
ConstructIdentifier(
|
||||
lemma: lemma.text,
|
||||
type: ConstructTypeEnum.morph,
|
||||
category: pos,
|
||||
),
|
||||
) ??
|
||||
ConstructUses(
|
||||
lemma: lemma.text,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
constructType: ConstructTypeEnum.morph,
|
||||
category: pos,
|
||||
uses: [],
|
||||
);
|
||||
}
|
||||
return vocab;
|
||||
}
|
||||
|
||||
ConstructUses morphConstruct(String morphFeature, String morphTag) =>
|
||||
MatrixState.pangeaController.getAnalytics.constructListModel
|
||||
.getConstructUses(
|
||||
ConstructIdentifier(
|
||||
lemma: morphTag,
|
||||
type: ConstructTypeEnum.morph,
|
||||
category: morphFeature,
|
||||
),
|
||||
) ??
|
||||
ConstructUses(
|
||||
lemma: morphTag,
|
||||
constructType: ConstructTypeEnum.morph,
|
||||
category: morphFeature,
|
||||
uses: [],
|
||||
);
|
||||
|
||||
int get xp {
|
||||
return constructs.fold<int>(
|
||||
|
|
@ -337,6 +460,12 @@ class PangeaToken {
|
|||
return DateTime.now().difference(lastUsed).inDays;
|
||||
}
|
||||
|
||||
int daysSinceLastUseMorph(String morphFeature, String morphTag) {
|
||||
final lastUsed = morphConstruct(morphFeature, morphTag).lastUsed;
|
||||
if (lastUsed == null) return 1000;
|
||||
return DateTime.now().difference(lastUsed).inDays;
|
||||
}
|
||||
|
||||
List<ConstructIdentifier> get _constructIDs {
|
||||
final List<ConstructIdentifier> ids = [];
|
||||
ids.add(
|
||||
|
|
@ -374,46 +503,91 @@ class PangeaToken {
|
|||
'target_types': eligibleActivityTypes.map((e) => e.string).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class PangeaTokenText {
|
||||
int offset;
|
||||
String content;
|
||||
int length;
|
||||
Future<List<String>> getEmojiChoices() => LemmaDictionaryRepo.get(
|
||||
LemmaDefinitionRequest(
|
||||
lemma: lemma.text,
|
||||
partOfSpeech: pos,
|
||||
lemmaLang: MatrixState
|
||||
.pangeaController.languageController.userL2?.langCode ??
|
||||
LanguageKeys.unknownLanguage,
|
||||
userL1: MatrixState
|
||||
.pangeaController.languageController.userL1?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
),
|
||||
).then((onValue) => onValue.emoji);
|
||||
|
||||
PangeaTokenText({
|
||||
required this.offset,
|
||||
required this.content,
|
||||
required this.length,
|
||||
});
|
||||
ConstructIdentifier get vocabConstructID => ConstructIdentifier(
|
||||
lemma: lemma.text,
|
||||
type: ConstructTypeEnum.vocab,
|
||||
category: pos,
|
||||
);
|
||||
|
||||
factory PangeaTokenText.fromJson(Map<String, dynamic> json) {
|
||||
debugger(when: kDebugMode && json[_offsetKey] == null);
|
||||
return PangeaTokenText(
|
||||
offset: json[_offsetKey],
|
||||
content: json[_contentKey],
|
||||
length: json[_lengthKey] ?? (json[_contentKey] as String).length,
|
||||
);
|
||||
}
|
||||
Room? get analyticsRoom {
|
||||
final String? l2 =
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
static const String _offsetKey = "offset";
|
||||
static const String _contentKey = "content";
|
||||
static const String _lengthKey = "length";
|
||||
|
||||
Map<String, dynamic> toJson() =>
|
||||
{_offsetKey: offset, _contentKey: content, _lengthKey: length};
|
||||
|
||||
//override equals and hashcode
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is PangeaTokenText) {
|
||||
return other.offset == offset &&
|
||||
other.content == content &&
|
||||
other.length == length;
|
||||
if (l2 == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return null;
|
||||
}
|
||||
return false;
|
||||
|
||||
final Room? analyticsRoom =
|
||||
MatrixState.pangeaController.matrixState.client.analyticsRoomLocal(l2);
|
||||
|
||||
if (analyticsRoom == null) {
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
|
||||
return analyticsRoom;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => offset.hashCode ^ content.hashCode ^ length.hashCode;
|
||||
/// [setEmoji] sets the emoji for the lemma
|
||||
/// NOTE: assumes that the language of the lemma is the same as the user's current l2
|
||||
Future<void> setEmoji(String emoji) async {
|
||||
if (analyticsRoom == null) return;
|
||||
try {
|
||||
final client = MatrixState.pangeaController.matrixState.client;
|
||||
final syncFuture = client.onRoomState.stream.firstWhere((event) {
|
||||
return event.roomId == analyticsRoom!.id &&
|
||||
event.state.type == PangeaEventTypes.userChosenEmoji;
|
||||
});
|
||||
client.setRoomStateWithKey(
|
||||
analyticsRoom!.id,
|
||||
PangeaEventTypes.userChosenEmoji,
|
||||
vocabConstructID.string,
|
||||
{ModelKey.emoji: emoji},
|
||||
);
|
||||
await syncFuture;
|
||||
} catch (err, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
data: {
|
||||
"construct": vocabConstructID.string,
|
||||
"emoji": emoji,
|
||||
},
|
||||
s: s,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [getEmoji] gets the emoji for the lemma
|
||||
/// NOTE: assumes that the language of the lemma is the same as the user's current l2
|
||||
String? getEmoji() {
|
||||
return analyticsRoom
|
||||
?.getState(PangeaEventTypes.userChosenEmoji, vocabConstructID.string)
|
||||
?.content
|
||||
.tryGet<String>(ModelKey.emoji);
|
||||
}
|
||||
|
||||
String get xpEmoji {
|
||||
if (xp < 5) {
|
||||
return "🌱";
|
||||
} else if (xp < 10) {
|
||||
return "🌿";
|
||||
} else {
|
||||
return "🌺";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
lib/pangea/models/pangea_token_text_model.dart
Normal file
45
lib/pangea/models/pangea_token_text_model.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class PangeaTokenText {
|
||||
int offset;
|
||||
String content;
|
||||
int length;
|
||||
|
||||
PangeaTokenText({
|
||||
required this.offset,
|
||||
required this.content,
|
||||
required this.length,
|
||||
});
|
||||
|
||||
factory PangeaTokenText.fromJson(Map<String, dynamic> json) {
|
||||
debugger(when: kDebugMode && json[_offsetKey] == null);
|
||||
return PangeaTokenText(
|
||||
offset: json[_offsetKey],
|
||||
content: json[_contentKey],
|
||||
length: json[_lengthKey] ?? (json[_contentKey] as String).length,
|
||||
);
|
||||
}
|
||||
|
||||
static const String _offsetKey = "offset";
|
||||
static const String _contentKey = "content";
|
||||
static const String _lengthKey = "length";
|
||||
|
||||
Map<String, dynamic> toJson() =>
|
||||
{_offsetKey: offset, _contentKey: content, _lengthKey: length};
|
||||
|
||||
//override equals and hashcode
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is PangeaTokenText) {
|
||||
return other.offset == offset &&
|
||||
other.content == content &&
|
||||
other.length == length;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => offset.hashCode ^ content.hashCode ^ length.hashCode;
|
||||
}
|
||||
|
|
@ -54,6 +54,7 @@ class MessageActivityRequest {
|
|||
|
||||
final List<PangeaToken> targetTokens;
|
||||
final ActivityTypeEnum targetType;
|
||||
final String? targetMorphFeature;
|
||||
|
||||
final ActivityQualityFeedback? activityQualityFeedback;
|
||||
|
||||
|
|
@ -65,6 +66,7 @@ class MessageActivityRequest {
|
|||
required this.activityQualityFeedback,
|
||||
required this.targetTokens,
|
||||
required this.targetType,
|
||||
required this.targetMorphFeature,
|
||||
}) {
|
||||
if (targetTokens.isEmpty) {
|
||||
throw Exception('Target tokens must not be empty');
|
||||
|
|
@ -87,6 +89,7 @@ class MessageActivityRequest {
|
|||
'activity_quality_feedback': activityQualityFeedback?.toJson(),
|
||||
'target_tokens': targetTokens.map((e) => e.toJson()).toList(),
|
||||
'target_type': targetType.string,
|
||||
'target_morph_feature': targetMorphFeature,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +102,8 @@ class MessageActivityRequest {
|
|||
other.targetType == targetType &&
|
||||
other.activityQualityFeedback?.feedbackText ==
|
||||
activityQualityFeedback?.feedbackText &&
|
||||
const ListEquality().equals(other.targetTokens, targetTokens);
|
||||
const ListEquality().equals(other.targetTokens, targetTokens) &&
|
||||
other.targetMorphFeature == targetMorphFeature;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -107,7 +111,8 @@ class MessageActivityRequest {
|
|||
return messageText.hashCode ^
|
||||
targetType.hashCode ^
|
||||
activityQualityFeedback.hashCode ^
|
||||
targetTokens.hashCode;
|
||||
targetTokens.hashCode ^
|
||||
targetMorphFeature.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -7,14 +8,16 @@ import 'package:flutter/material.dart';
|
|||
|
||||
class ActivityContent {
|
||||
final String question;
|
||||
|
||||
/// choices, including the correct answer
|
||||
final List<String> choices;
|
||||
final String answer;
|
||||
final List<String> answers;
|
||||
final RelevantSpanDisplayDetails? spanDisplayDetails;
|
||||
|
||||
ActivityContent({
|
||||
required this.question,
|
||||
required this.choices,
|
||||
required this.answer,
|
||||
required this.answers,
|
||||
required this.spanDisplayDetails,
|
||||
});
|
||||
|
||||
|
|
@ -25,27 +28,45 @@ class ActivityContent {
|
|||
if (value != choices[index]) {
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
return value == answer || index == correctAnswerIndex;
|
||||
return answers.contains(value) || correctAnswerIndices.contains(index);
|
||||
}
|
||||
|
||||
bool get isValidQuestion => choices.contains(answer);
|
||||
bool get isValidQuestion => choices.toSet().containsAll(answers);
|
||||
|
||||
int get correctAnswerIndex => choices.indexOf(answer);
|
||||
List<int> get correctAnswerIndices {
|
||||
final List<int> indices = [];
|
||||
for (var i = 0; i < choices.length; i++) {
|
||||
if (answers.contains(choices[i])) {
|
||||
indices.add(i);
|
||||
}
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
|
||||
int choiceIndex(String choice) => choices.indexOf(choice);
|
||||
|
||||
Color choiceColor(int index) =>
|
||||
index == correctAnswerIndex ? AppConfig.success : AppConfig.warning;
|
||||
Color choiceColor(int index) => correctAnswerIndices.contains(index)
|
||||
? AppConfig.success
|
||||
: AppConfig.warning;
|
||||
|
||||
factory ActivityContent.fromJson(Map<String, dynamic> json) {
|
||||
final spanDisplay = json['span_display_details'] != null &&
|
||||
json['span_display_details'] is Map
|
||||
? RelevantSpanDisplayDetails.fromJson(json['span_display_details'])
|
||||
: null;
|
||||
|
||||
final answerEntry = json['answer'] ?? json['correct_answer'] ?? "";
|
||||
List<String> answers = [];
|
||||
if (answerEntry is String) {
|
||||
answers = [answerEntry];
|
||||
} else if (answerEntry is List) {
|
||||
answers = answerEntry.map((e) => e as String).toList();
|
||||
}
|
||||
|
||||
return ActivityContent(
|
||||
question: json['question'] as String,
|
||||
choices: (json['choices'] as List).map((e) => e as String).toList(),
|
||||
answer: json['answer'] ?? json['correct_answer'] as String,
|
||||
answers: answers,
|
||||
spanDisplayDetails: spanDisplay,
|
||||
);
|
||||
}
|
||||
|
|
@ -54,7 +75,7 @@ class ActivityContent {
|
|||
return {
|
||||
'question': question,
|
||||
'choices': choices,
|
||||
'answer': answer,
|
||||
'answer': answers,
|
||||
'span_display_details': spanDisplayDetails?.toJson(),
|
||||
};
|
||||
}
|
||||
|
|
@ -67,11 +88,11 @@ class ActivityContent {
|
|||
return other is ActivityContent &&
|
||||
other.question == question &&
|
||||
other.choices == choices &&
|
||||
other.answer == answer;
|
||||
const ListEquality().equals(other.answers.sorted(), answers.sorted());
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return question.hashCode ^ choices.hashCode ^ answer.hashCode;
|
||||
return question.hashCode ^ choices.hashCode ^ Object.hashAll(answers);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,7 +230,8 @@ class PracticeActivityModel {
|
|||
|
||||
bool get shouldPlayTargetTokens =>
|
||||
targetTokens != null &&
|
||||
activityType != ActivityTypeEnum.hiddenWordListening;
|
||||
activityType != ActivityTypeEnum.hiddenWordListening &&
|
||||
activityType != ActivityTypeEnum.morphId;
|
||||
|
||||
factory PracticeActivityModel.fromJson(Map<String, dynamic> json) {
|
||||
// moving from multiple_choice to content as the key
|
||||
|
|
|
|||
|
|
@ -143,6 +143,16 @@ class ActivityRecordResponse {
|
|||
return score > 0
|
||||
? ConstructUseTypeEnum.corWL
|
||||
: ConstructUseTypeEnum.incWL;
|
||||
case ActivityTypeEnum.emoji:
|
||||
return ConstructUseTypeEnum.em;
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
return score > 0
|
||||
? ConstructUseTypeEnum.corL
|
||||
: ConstructUseTypeEnum.incL;
|
||||
case ActivityTypeEnum.morphId:
|
||||
return score > 0
|
||||
? ConstructUseTypeEnum.corM
|
||||
: ConstructUseTypeEnum.incM;
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return score > 0
|
||||
? ConstructUseTypeEnum.corHWL
|
||||
|
|
@ -155,9 +165,28 @@ class ActivityRecordResponse {
|
|||
PracticeActivityModel practiceActivity,
|
||||
ConstructUseMetaData metadata,
|
||||
) {
|
||||
if (practiceActivity.activityType == ActivityTypeEnum.emoji) {
|
||||
if (practiceActivity.targetTokens != null &&
|
||||
practiceActivity.targetTokens!.isNotEmpty) {
|
||||
final token = practiceActivity.targetTokens!.first;
|
||||
return [
|
||||
OneConstructUse(
|
||||
lemma: token.lemma.text,
|
||||
form: token.text.content,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
useType: useType(practiceActivity.activityType),
|
||||
metadata: metadata,
|
||||
category: token.pos,
|
||||
),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
if (practiceActivity.targetTokens == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final uses = practiceActivity.targetTokens!
|
||||
.map(
|
||||
(token) => OneConstructUse(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ class PApiUrls {
|
|||
static String accountEndpoint =
|
||||
"${Environment.choreoApi}${PApiUrls.accountPrefix}";
|
||||
|
||||
/// ---------------------- Util --------------------------------------
|
||||
static String appVersion = "${PApiUrls.choreoEndpoint}/version";
|
||||
|
||||
/// ---------------------- Languages --------------------------------------
|
||||
static String getLanguages = "${PApiUrls.choreoEndpoint}/languages";
|
||||
|
||||
|
|
@ -61,6 +64,8 @@ class PApiUrls {
|
|||
static String messageActivityGeneration =
|
||||
"${PApiUrls.choreoEndpoint}/practice";
|
||||
|
||||
static String lemmaDictionary = "${PApiUrls.choreoEndpoint}/lemma_definition";
|
||||
|
||||
///-------------------------------- revenue cat --------------------------
|
||||
|
||||
static String rcApiV1 = "https://api.revenuecat.com/v1";
|
||||
|
|
@ -70,6 +75,4 @@ class PApiUrls {
|
|||
"${PApiUrls.subscriptionEndpoint}/all_products";
|
||||
|
||||
static String rcSubscription = "$rcApiV1/subscribers";
|
||||
|
||||
static String appVersion = "${PApiUrls.choreoEndpoint}/version";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../config/environment.dart';
|
||||
import '../models/pangea_token_model.dart';
|
||||
import '../network/requests.dart';
|
||||
import '../network/urls.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:fluffychat/pangea/models/language_detection_model.dart';
|
|||
import 'package:fluffychat/pangea/models/lemma.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/span_data_repo.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class GenerateImageeResponse {
|
|||
}
|
||||
|
||||
factory GenerateImageeResponse.error() {
|
||||
// TODO: Implement better error handling
|
||||
return GenerateImageeResponse(
|
||||
imageUrl: 'https://i.imgur.com/2L2JYqk.png',
|
||||
prompt: 'Error',
|
||||
|
|
|
|||
147
lib/pangea/repo/lemma_definition_repo.dart
Normal file
147
lib/pangea/repo/lemma_definition_repo.dart
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/network/urls.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import '../config/environment.dart';
|
||||
import '../network/requests.dart';
|
||||
|
||||
class LemmaDefinitionRequest {
|
||||
final String lemma;
|
||||
final String partOfSpeech;
|
||||
final String lemmaLang;
|
||||
final String userL1;
|
||||
|
||||
LemmaDefinitionRequest({
|
||||
required this.lemma,
|
||||
required this.partOfSpeech,
|
||||
required this.lemmaLang,
|
||||
required this.userL1,
|
||||
});
|
||||
|
||||
factory LemmaDefinitionRequest.fromJson(Map<String, dynamic> json) {
|
||||
return LemmaDefinitionRequest(
|
||||
lemma: json['lemma'] as String,
|
||||
partOfSpeech: json['part_of_speech'] as String,
|
||||
lemmaLang: json['lemma_lang'] as String,
|
||||
userL1: json['user_l1'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'lemma': lemma,
|
||||
'part_of_speech': partOfSpeech,
|
||||
'lemma_lang': lemmaLang,
|
||||
'user_l1': userL1,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is LemmaDefinitionRequest &&
|
||||
runtimeType == other.runtimeType &&
|
||||
lemma == other.lemma &&
|
||||
partOfSpeech == other.partOfSpeech &&
|
||||
lemmaLang == other.lemmaLang &&
|
||||
userL1 == other.userL1;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
lemma.hashCode ^
|
||||
partOfSpeech.hashCode ^
|
||||
lemmaLang.hashCode ^
|
||||
userL1.hashCode;
|
||||
}
|
||||
|
||||
class LemmaDefinitionResponse {
|
||||
final List<String> emoji;
|
||||
final String definition;
|
||||
|
||||
LemmaDefinitionResponse({
|
||||
required this.emoji,
|
||||
required this.definition,
|
||||
});
|
||||
|
||||
factory LemmaDefinitionResponse.fromJson(Map<String, dynamic> json) {
|
||||
return LemmaDefinitionResponse(
|
||||
emoji: (json['emoji'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
definition: json['definition'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'emoji': emoji,
|
||||
'definition': definition,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is LemmaDefinitionResponse &&
|
||||
runtimeType == other.runtimeType &&
|
||||
emoji.length == other.emoji.length &&
|
||||
emoji.every((element) => other.emoji.contains(element)) &&
|
||||
definition == other.definition;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
emoji.fold(0, (prev, element) => prev ^ element.hashCode) ^
|
||||
definition.hashCode;
|
||||
}
|
||||
|
||||
class LemmaDictionaryRepo {
|
||||
// In-memory cache with timestamps
|
||||
static final Map<LemmaDefinitionRequest, LemmaDefinitionResponse> _cache = {};
|
||||
static final Map<LemmaDefinitionRequest, DateTime> _cacheTimestamps = {};
|
||||
|
||||
static const Duration _cacheDuration = Duration(days: 2);
|
||||
|
||||
static Future<LemmaDefinitionResponse> get(
|
||||
LemmaDefinitionRequest request,
|
||||
) async {
|
||||
_clearExpiredEntries();
|
||||
|
||||
// Check the cache first
|
||||
if (_cache.containsKey(request)) {
|
||||
return _cache[request]!;
|
||||
}
|
||||
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
);
|
||||
|
||||
final requestBody = request.toJson();
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.lemmaDictionary,
|
||||
body: requestBody,
|
||||
);
|
||||
|
||||
final decodedBody = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
final response = LemmaDefinitionResponse.fromJson(decodedBody);
|
||||
|
||||
// Store the response and timestamp in the cache
|
||||
_cache[request] = response;
|
||||
_cacheTimestamps[request] = DateTime.now();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static void _clearExpiredEntries() {
|
||||
final now = DateTime.now();
|
||||
final expiredKeys = _cacheTimestamps.entries
|
||||
.where((entry) => now.difference(entry.value) > _cacheDuration)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
|
||||
for (final key in expiredKeys) {
|
||||
_cache.remove(key);
|
||||
_cacheTimestamps.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
lib/pangea/repo/practice/emoji_activity_generator.dart
Normal file
36
lib/pangea/repo/practice/emoji_activity_generator.dart
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class EmojiActivityGenerator {
|
||||
Future<MessageActivityResponse> get(
|
||||
MessageActivityRequest req,
|
||||
) async {
|
||||
debugger(when: kDebugMode && req.targetTokens.length != 1);
|
||||
|
||||
final PangeaToken token = req.targetTokens.first;
|
||||
|
||||
final List<String> emojis = await token.getEmojiChoices();
|
||||
|
||||
// TODO - modify MultipleChoiceActivity flow to allow no correct answer
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activityType: ActivityTypeEnum.emoji,
|
||||
targetTokens: [token],
|
||||
tgtConstructs: [token.vocabConstructID],
|
||||
langCode: req.userL2,
|
||||
content: ActivityContent(
|
||||
question: "",
|
||||
choices: emojis,
|
||||
answers: emojis,
|
||||
spanDisplayDetails: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
37
lib/pangea/repo/practice/lemma_activity_generator.dart
Normal file
37
lib/pangea/repo/practice/lemma_activity_generator.dart
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class LemmaActivityGenerator {
|
||||
Future<MessageActivityResponse> get(
|
||||
MessageActivityRequest req,
|
||||
) async {
|
||||
debugger(when: kDebugMode && req.targetTokens.length != 1);
|
||||
|
||||
final token = req.targetTokens.first;
|
||||
final List<String> choices = await MatrixState
|
||||
.pangeaController.getAnalytics.constructListModel
|
||||
.lemmaActivityDistractors(token);
|
||||
|
||||
// TODO - modify MultipleChoiceActivity flow to allow no correct answer
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
activityType: ActivityTypeEnum.lemmaId,
|
||||
targetTokens: [token],
|
||||
tgtConstructs: [token.vocabConstructID],
|
||||
langCode: req.userL2,
|
||||
content: ActivityContent(
|
||||
question: "",
|
||||
choices: choices,
|
||||
answers: [token.lemma.text],
|
||||
spanDisplayDetails: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
100
lib/pangea/repo/practice/morph_activity_generator.dart
Normal file
100
lib/pangea/repo/practice/morph_activity_generator.dart
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
typedef MorphActivitySequence = Map<String, POSActivitySequence>;
|
||||
|
||||
typedef POSActivitySequence = List<String>;
|
||||
|
||||
class MorphActivityGenerator {
|
||||
// TODO we want to define this on the server and have the client pull it down
|
||||
final Map<String, MorphActivitySequence> sequence = {
|
||||
"en": {
|
||||
"ADJ": ["AdvType", "Aspect"],
|
||||
"ADP": [],
|
||||
"ADV": [],
|
||||
"AUX": ["Tense", "Number"],
|
||||
"CCONJ": [],
|
||||
"DET": [],
|
||||
"NOUN": ["Number"],
|
||||
"NUM": [],
|
||||
"PRON": ["Number", "Person"],
|
||||
"SCONJ": [],
|
||||
"PUNCT": [],
|
||||
"VERB": ["Tense", "Aspect"],
|
||||
},
|
||||
};
|
||||
|
||||
/// Get the sequence of activities for a given part of speech
|
||||
/// The sequence is a list of morphological features that should be practiced
|
||||
/// in order for the given part of speech
|
||||
Future<POSActivitySequence> getSequence(String langCode, String pos) async {
|
||||
if (!sequence.containsKey(langCode)) {
|
||||
langCode = "en";
|
||||
}
|
||||
final MorphActivitySequence morphActivitySequence = sequence[langCode]!;
|
||||
|
||||
if (!morphActivitySequence.containsKey(pos)) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "No sequence defined",
|
||||
data: {"langCode": langCode, "pos": pos},
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
return morphActivitySequence[pos]!;
|
||||
}
|
||||
|
||||
/// Generate a morphological activity for a given token and morphological feature
|
||||
Future<MessageActivityResponse> get(
|
||||
MessageActivityRequest req,
|
||||
) async {
|
||||
debugger(when: kDebugMode && req.targetTokens.length != 1);
|
||||
|
||||
debugger(when: kDebugMode && req.targetMorphFeature == null);
|
||||
|
||||
final PangeaToken token = req.targetTokens.first;
|
||||
|
||||
final String morphFeature = req.targetMorphFeature!;
|
||||
final String? morphTag = token.getMorphTag(morphFeature);
|
||||
|
||||
if (morphTag == null) {
|
||||
debugger(when: kDebugMode);
|
||||
throw "No morph tag found for morph feature";
|
||||
}
|
||||
|
||||
final List<String> distractors = MatrixState
|
||||
.pangeaController.getAnalytics.constructListModel
|
||||
.morphActivityDistractors(morphFeature, morphTag);
|
||||
|
||||
return MessageActivityResponse(
|
||||
activity: PracticeActivityModel(
|
||||
tgtConstructs: [
|
||||
ConstructIdentifier(
|
||||
lemma: morphTag,
|
||||
type: ConstructTypeEnum.morph,
|
||||
category: morphFeature,
|
||||
),
|
||||
],
|
||||
targetTokens: req.targetTokens,
|
||||
langCode: req.userL2,
|
||||
activityType: ActivityTypeEnum.morphId,
|
||||
content: ActivityContent(
|
||||
question: "",
|
||||
choices: distractors + [morphTag],
|
||||
answers: [morphTag],
|
||||
spanDisplayDetails: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import 'dart:developer';
|
|||
import 'package:fluffychat/pangea/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
|
|
@ -12,14 +13,19 @@ import 'package:fluffychat/pangea/models/practice_activities.dart/message_activi
|
|||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/network/requests.dart';
|
||||
import 'package:fluffychat/pangea/network/urls.dart';
|
||||
import 'package:fluffychat/pangea/repo/practice/emoji_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/repo/practice/lemma_activity_generator.dart';
|
||||
import 'package:fluffychat/pangea/repo/practice/morph_activity_generator.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// Represents an item in the completion cache.
|
||||
class _RequestCacheItem {
|
||||
MessageActivityRequest req;
|
||||
PracticeActivityModelResponse? practiceActivity;
|
||||
final MessageActivityRequest req;
|
||||
final PracticeActivityModelResponse practiceActivity;
|
||||
final DateTime createdAt = DateTime.now();
|
||||
|
||||
_RequestCacheItem({
|
||||
required this.req,
|
||||
|
|
@ -34,18 +40,29 @@ class PracticeGenerationController {
|
|||
|
||||
late PangeaController _pangeaController;
|
||||
|
||||
PracticeGenerationController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
final MorphActivityGenerator _morph = MorphActivityGenerator();
|
||||
final EmojiActivityGenerator _emoji = EmojiActivityGenerator();
|
||||
final LemmaActivityGenerator _lemma = LemmaActivityGenerator();
|
||||
|
||||
PracticeGenerationController() {
|
||||
_pangeaController = MatrixState.pangeaController;
|
||||
_initializeCacheClearing();
|
||||
}
|
||||
|
||||
void _initializeCacheClearing() {
|
||||
const duration = Duration(minutes: 2);
|
||||
const duration = Duration(minutes: 10);
|
||||
_cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache());
|
||||
}
|
||||
|
||||
void _clearCache() {
|
||||
_cache.clear();
|
||||
final now = DateTime.now();
|
||||
final keys = _cache.keys.toList();
|
||||
for (final key in keys) {
|
||||
final item = _cache[key]!;
|
||||
if (now.difference(item.createdAt) > const Duration(minutes: 10)) {
|
||||
_cache.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
|
|
@ -72,7 +89,7 @@ class PracticeGenerationController {
|
|||
);
|
||||
}
|
||||
|
||||
Future<MessageActivityResponse> _fetch({
|
||||
Future<MessageActivityResponse> _fetchFromServer({
|
||||
required String accessToken,
|
||||
required MessageActivityRequest requestModel,
|
||||
}) async {
|
||||
|
|
@ -97,9 +114,31 @@ class PracticeGenerationController {
|
|||
}
|
||||
}
|
||||
|
||||
//TODO - allow return of activity content before sending the event
|
||||
// this requires some downstream changes to the way the event is handled
|
||||
Future<PracticeActivityModelResponse?> getPracticeActivity(
|
||||
Future<MessageActivityResponse> _routePracticeActivity({
|
||||
required String accessToken,
|
||||
required MessageActivityRequest req,
|
||||
}) async {
|
||||
// some activities we'll get from the server and others we'll generate locally
|
||||
switch (req.targetType) {
|
||||
case ActivityTypeEnum.emoji:
|
||||
return _emoji.get(req);
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
return _lemma.get(req);
|
||||
case ActivityTypeEnum.morphId:
|
||||
return _morph.get(req);
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
// TODO bring clientside because more efficient
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
// TODO get correct answer with translation and distractors with distractor service
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return _fetchFromServer(
|
||||
accessToken: accessToken,
|
||||
requestModel: req,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<PracticeActivityModelResponse> getPracticeActivity(
|
||||
MessageActivityRequest req,
|
||||
PangeaMessageEvent event,
|
||||
) async {
|
||||
|
|
@ -111,11 +150,13 @@ class PracticeGenerationController {
|
|||
return _cache[cacheKey]!.practiceActivity;
|
||||
}
|
||||
|
||||
final MessageActivityResponse res = await _fetch(
|
||||
final MessageActivityResponse res = await _routePracticeActivity(
|
||||
accessToken: _pangeaController.userController.accessToken,
|
||||
requestModel: req,
|
||||
req: req,
|
||||
);
|
||||
|
||||
// TODO resolve some wierdness here whereby the activity can be null but then... it's not
|
||||
|
||||
final eventCompleter = Completer<PracticeActivityEvent?>();
|
||||
|
||||
debugPrint('Activity generated: ${res.activity.toJson()}');
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
|
|
@ -454,7 +451,7 @@ String? getGrammarCopy({
|
|||
);
|
||||
return L10n.of(context).grammarCopyUnknown;
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
// debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: 'Need to add copy to intl_en.arb',
|
||||
data: {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import 'package:fluffychat/pages/chat/events/audio_player.dart';
|
|||
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
|
||||
|
|
@ -185,7 +185,6 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
alignment: Alignment.center,
|
||||
child: _isLoading
|
||||
? const ToolbarContentLoadingIndicator()
|
||||
|
|
@ -199,6 +198,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
setIsPlayingAudio: widget.setIsPlayingAudio,
|
||||
fontSize:
|
||||
AppConfig.messageFontSize * AppConfig.fontSizeFactor,
|
||||
padding: 0,
|
||||
)
|
||||
: const CardErrorWidget(
|
||||
error: "Null audio file in message_audio_card",
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/message_reactions.dart';
|
||||
import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_display_instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar_buttons.dart';
|
||||
|
|
@ -20,7 +20,6 @@ import 'package:fluffychat/pangea/widgets/chat/overlay_header.dart';
|
|||
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
|
||||
import 'package:fluffychat/widgets/avatar.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';
|
||||
|
|
@ -58,7 +57,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
StreamSubscription? _reactionSubscription;
|
||||
Animation<double>? _overlayPositionAnimation;
|
||||
|
||||
MessageMode toolbarMode = MessageMode.translation;
|
||||
MessageMode toolbarMode = MessageMode.noneSelected;
|
||||
PangeaTokenText? _selectedSpan;
|
||||
|
||||
List<PangeaToken>? tokens;
|
||||
|
|
@ -74,33 +73,30 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
|
||||
int get activitiesLeftToComplete => messageAnalyticsEntry?.numActivities ?? 0;
|
||||
|
||||
bool get isPracticeComplete => activitiesLeftToComplete <= 0;
|
||||
bool get isPracticeComplete =>
|
||||
activitiesLeftToComplete <= 0 || !messageInUserL2;
|
||||
|
||||
/// Decides whether an _initialSelectedToken should be used
|
||||
/// for a first practice activity on the word meaning
|
||||
PangeaToken? get _selectedTargetTokenForWordMeaning {
|
||||
void _initializeSelectedToken() {
|
||||
// if there is no initial selected token, then we don't need to do anything
|
||||
if (widget._initialSelectedToken == null || messageAnalyticsEntry == null) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
"selected token ${widget._initialSelectedToken?.analyticsDebugPrint}",
|
||||
);
|
||||
debugPrint(
|
||||
"${widget._initialSelectedToken?.vocabConstruct.uses.map((u) => "${u.useType} ${u.timeStamp}").join(", ")}",
|
||||
);
|
||||
|
||||
// should not already be involved in a hidden word activity
|
||||
final isInHiddenWordActivity =
|
||||
messageAnalyticsEntry!.isTokenInHiddenWordActivity(
|
||||
widget._initialSelectedToken!,
|
||||
);
|
||||
// whether the activity should generally be involved in an activity
|
||||
// final shouldDoActivity = widget._initialSelectedToken!
|
||||
// .shouldDoActivity(ActivityTypeEnum.wordMeaning);
|
||||
|
||||
return !isInHiddenWordActivity ? widget._initialSelectedToken : null;
|
||||
// whether the activity should generally be involved in an activity
|
||||
final selected =
|
||||
!isInHiddenWordActivity ? widget._initialSelectedToken : null;
|
||||
|
||||
if (selected != null) {
|
||||
_updateSelectedSpan(selected.text);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -111,6 +107,23 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
_setupSubscriptions();
|
||||
}
|
||||
|
||||
void _updateSelectedSpan(PangeaTokenText selectedSpan) {
|
||||
_selectedSpan = selectedSpan;
|
||||
|
||||
widget.chatController.choreographer.tts.tryToSpeak(
|
||||
selectedSpan.content,
|
||||
context,
|
||||
widget._pangeaMessageEvent?.eventId,
|
||||
);
|
||||
|
||||
// if a token is selected, then the toolbar should be in wordZoom mode
|
||||
if (toolbarMode != MessageMode.wordZoom) {
|
||||
debugPrint("_updateSelectedSpan: setting toolbarMode to wordZoom");
|
||||
updateToolbarMode(MessageMode.wordZoom);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _setupSubscriptions() {
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
|
|
@ -165,11 +178,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
},
|
||||
);
|
||||
} finally {
|
||||
if (_selectedTargetTokenForWordMeaning != null) {
|
||||
messageAnalyticsEntry?.addForWordMeaning(
|
||||
_selectedTargetTokenForWordMeaning!,
|
||||
);
|
||||
}
|
||||
_initializeSelectedToken();
|
||||
_setInitialToolbarMode();
|
||||
initialized = true;
|
||||
if (mounted) setState(() {});
|
||||
|
|
@ -186,22 +195,24 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
return setState(() {});
|
||||
}
|
||||
|
||||
// 1) we're only going to do activities if we have tokens for the message
|
||||
// 2) if the user selects a span on initialization, then we want to give
|
||||
// them a practice activity on that word
|
||||
// 3) if the user has activities left to complete, then we want to give them
|
||||
if (tokens != null && activitiesLeftToComplete > 0 && messageInUserL2) {
|
||||
// 1) if we have a hidden word activity, then we should start with that
|
||||
if (messageAnalyticsEntry?.nextActivity?.activityType ==
|
||||
ActivityTypeEnum.hiddenWordListening) {
|
||||
return setState(() => toolbarMode = MessageMode.practiceActivity);
|
||||
}
|
||||
|
||||
if (selectedToken != null) {
|
||||
return setState(() => toolbarMode = MessageMode.wordZoom);
|
||||
}
|
||||
|
||||
// Note: this setting is now hidden so this will always be false
|
||||
// leaving this here in case we want to bring it back
|
||||
if (MatrixState.pangeaController.userController.profile.userSettings
|
||||
.autoPlayMessages) {
|
||||
return setState(() => toolbarMode = MessageMode.textToSpeech);
|
||||
}
|
||||
// if (MatrixState.pangeaController.userController.profile.userSettings
|
||||
// .autoPlayMessages) {
|
||||
// return setState(() => toolbarMode = MessageMode.textToSpeech);
|
||||
// }
|
||||
|
||||
setState(() => toolbarMode = MessageMode.translation);
|
||||
// defaults to noneSelected
|
||||
}
|
||||
|
||||
/// We need to check if the setState call is safe to call immediately
|
||||
|
|
@ -242,30 +253,30 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
/// When an activity is completed, we need to update the state
|
||||
/// and check if the toolbar should be unlocked
|
||||
void onActivityFinish() {
|
||||
messageAnalyticsEntry!.onActivityComplete();
|
||||
if (!mounted) return;
|
||||
_clearSelection();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
/// In some cases, we need to exit the practice flow and let the user
|
||||
/// interact with the toolbar without completing activities
|
||||
void exitPracticeFlow() {
|
||||
messageAnalyticsEntry?.clearActivityQueue();
|
||||
_clearSelection();
|
||||
messageAnalyticsEntry?.exitPracticeFlow();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void updateToolbarMode(MessageMode mode) {
|
||||
setState(() {
|
||||
// only practiceActivity and wordZoom make sense with selectedSpan
|
||||
if (![MessageMode.practiceActivity, MessageMode.wordZoom]
|
||||
.contains(mode)) {
|
||||
debugPrint("updateToolbarMode: $mode - clearing selectedSpan");
|
||||
_selectedSpan = null;
|
||||
}
|
||||
toolbarMode = mode;
|
||||
});
|
||||
}
|
||||
|
||||
void _clearSelection() {
|
||||
_selectedSpan = null;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
/// The text that the toolbar should target
|
||||
/// If there is no selectedSpan, then the whole message is the target
|
||||
/// If there is a selectedSpan, then the target is the selected text
|
||||
|
|
@ -284,69 +295,42 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
void onClickOverlayMessageToken(
|
||||
PangeaToken token,
|
||||
) {
|
||||
if ([
|
||||
MessageMode.practiceActivity,
|
||||
// MessageMode.textToSpeech
|
||||
].contains(toolbarMode) ||
|
||||
isPlayingAudio) {
|
||||
if (toolbarMode == MessageMode.practiceActivity &&
|
||||
messageAnalyticsEntry?.nextActivity?.activityType ==
|
||||
ActivityTypeEnum.hiddenWordListening) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if there's no selected span, then select the token
|
||||
if (_selectedSpan == null) {
|
||||
_selectedSpan = token.text;
|
||||
} else {
|
||||
// if there is a selected span, then deselect the token if it's the same
|
||||
if (isTokenSelected(token)) {
|
||||
_selectedSpan = null;
|
||||
} else {
|
||||
// if there is a selected span but it is not the same, then select the token
|
||||
_selectedSpan = token.text;
|
||||
}
|
||||
}
|
||||
|
||||
if (_selectedSpan != null) {
|
||||
widget.chatController.choreographer.tts.tryToSpeak(
|
||||
token.text.content,
|
||||
context,
|
||||
pangeaMessageEvent!.eventId,
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setSelectedSpan(PracticeActivityModel activity) {
|
||||
if (pangeaMessageEvent == null) return;
|
||||
|
||||
final RelevantSpanDisplayDetails? span =
|
||||
activity.content.spanDisplayDetails;
|
||||
|
||||
if (span == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (span.displayInstructions != ActivityDisplayInstructionsEnum.nothing) {
|
||||
_selectedSpan = PangeaTokenText(
|
||||
offset: span.offset,
|
||||
length: span.length,
|
||||
content: widget._pangeaMessageEvent!.messageDisplayText
|
||||
.substring(span.offset, span.offset + span.length),
|
||||
);
|
||||
} else {
|
||||
_selectedSpan = null;
|
||||
}
|
||||
// PangeaTokenText? newSelectedSpan;
|
||||
// if (_selectedSpan == null) {
|
||||
// newSelectedSpan = token.text;
|
||||
// } else {
|
||||
// // if there is a selected span, then deselect the token if it's the same
|
||||
// if (isTokenSelected(token)) {
|
||||
// newSelectedSpan = null;
|
||||
// } else {
|
||||
// // if there is a selected span but it is not the same, then select the token
|
||||
// newSelectedSpan = token.text;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (newSelectedSpan != null) {
|
||||
// updateToolbarMode(MessageMode.practiceActivity);
|
||||
// }
|
||||
_updateSelectedSpan(token.text);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
/// Whether the given token is currently selected
|
||||
bool isTokenSelected(PangeaToken token) {
|
||||
return _selectedSpan?.offset == token.text.offset &&
|
||||
final isSelected = _selectedSpan?.offset == token.text.offset &&
|
||||
_selectedSpan?.length == token.text.length;
|
||||
return isSelected;
|
||||
}
|
||||
|
||||
PangeaToken? get selectedToken => tokens?.firstWhereOrNull(isTokenSelected);
|
||||
|
||||
/// Whether the overlay is currently displaying a selection
|
||||
bool get isSelection => _selectedSpan != null;
|
||||
|
||||
|
|
@ -394,7 +378,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
_belowMessageHeight;
|
||||
|
||||
final bool hasHeaderOverflow =
|
||||
_messageOffset!.dy < (AppConfig.toolbarMaxHeight + _headerHeight);
|
||||
_messageOffset!.dy < (AppConfig.toolbarMaxHeight + _headerHeight + 10);
|
||||
final bool hasFooterOverflow = (_footerHeight + 5) > currentBottomOffset;
|
||||
|
||||
if (!hasHeaderOverflow && !hasFooterOverflow) return;
|
||||
|
|
@ -418,7 +402,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
final remainingSpace = _screenHeight! - totalTopOffset;
|
||||
if (remainingSpace < _headerHeight) {
|
||||
// the overlay could run over the header, so it needs to be shifted down
|
||||
animationEndOffset -= (_headerHeight - remainingSpace);
|
||||
animationEndOffset -= (_headerHeight - remainingSpace + 10);
|
||||
}
|
||||
scrollOffset = animationEndOffset - currentBottomOffset;
|
||||
} else if (hasFooterOverflow) {
|
||||
|
|
@ -591,7 +575,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
if (pangeaMessageEvent != null)
|
||||
MessageToolbar(
|
||||
pangeaMessageEvent: pangeaMessageEvent!,
|
||||
overLayController: this,
|
||||
overlayController: this,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
|
|
@ -624,7 +608,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
ToolbarButtons(
|
||||
event: widget._event,
|
||||
overlayController: this,
|
||||
width: 250,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -695,7 +678,10 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
OverlayFooter(controller: widget.chatController),
|
||||
OverlayFooter(
|
||||
controller: widget.chatController,
|
||||
overlayController: this,
|
||||
),
|
||||
SizedBox(height: _mediaQuery?.padding.bottom ?? 0),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart';
|
||||
|
|
@ -12,143 +9,91 @@ import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart';
|
|||
import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/message_display_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/word_zoom/word_zoom_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';
|
||||
import 'package:matrix/matrix_api_lite/model/message_types.dart';
|
||||
|
||||
const double minCardHeight = 70;
|
||||
|
||||
class MessageToolbar extends StatelessWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final MessageOverlayController overLayController;
|
||||
final MessageOverlayController overlayController;
|
||||
|
||||
const MessageToolbar({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.overLayController,
|
||||
required this.overlayController,
|
||||
});
|
||||
|
||||
TtsController get ttsController =>
|
||||
overLayController.widget.chatController.choreographer.tts;
|
||||
overlayController.widget.chatController.choreographer.tts;
|
||||
|
||||
Widget toolbarContent(BuildContext context) {
|
||||
Widget? toolbarContent(BuildContext context) {
|
||||
final bool subscribed =
|
||||
MatrixState.pangeaController.subscriptionController.isSubscribed;
|
||||
|
||||
if (!subscribed) {
|
||||
return MessageUnsubscribedCard(
|
||||
controller: overLayController,
|
||||
controller: overlayController,
|
||||
);
|
||||
}
|
||||
|
||||
if (!overLayController.initialized) {
|
||||
if (overlayController.messageAnalyticsEntry?.hasHiddenWordActivity ??
|
||||
false) {
|
||||
return PracticeActivityCard(
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
overlayController: overlayController,
|
||||
targetTokensAndActivityType:
|
||||
overlayController.messageAnalyticsEntry!.nextActivity!,
|
||||
);
|
||||
}
|
||||
|
||||
if (!overlayController.initialized) {
|
||||
return const ToolbarContentLoadingIndicator();
|
||||
}
|
||||
|
||||
switch (overLayController.toolbarMode) {
|
||||
switch (overlayController.toolbarMode) {
|
||||
case MessageMode.translation:
|
||||
return MessageTranslationCard(
|
||||
messageEvent: pangeaMessageEvent,
|
||||
selection: overLayController.selectedSpan,
|
||||
selection: overlayController.selectedSpan,
|
||||
);
|
||||
case MessageMode.textToSpeech:
|
||||
return MessageAudioCard(
|
||||
messageEvent: pangeaMessageEvent,
|
||||
overlayController: overLayController,
|
||||
selection: overLayController.selectedSpan,
|
||||
overlayController: overlayController,
|
||||
selection: overlayController.selectedSpan,
|
||||
tts: ttsController,
|
||||
setIsPlayingAudio: overLayController.setIsPlayingAudio,
|
||||
setIsPlayingAudio: overlayController.setIsPlayingAudio,
|
||||
);
|
||||
case MessageMode.speechToText:
|
||||
return MessageSpeechToTextCard(
|
||||
messageEvent: pangeaMessageEvent,
|
||||
);
|
||||
case MessageMode.definition:
|
||||
if (!overLayController.isSelection) {
|
||||
return FutureBuilder(
|
||||
//TODO - convert this to synchronous if possible
|
||||
future: Future.value(
|
||||
pangeaMessageEvent.messageDisplayRepresentation?.tokens,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const ToolbarContentLoadingIndicator();
|
||||
} else if (snapshot.hasError ||
|
||||
snapshot.data == null ||
|
||||
snapshot.data!.isEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: CardErrorWidget(
|
||||
error: "No tokens available",
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return MessageDisplayCard(
|
||||
displayText: L10n.of(context).selectToDefine,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
final selectedText = overLayController.targetText;
|
||||
|
||||
return WordDataCard(
|
||||
word: selectedText,
|
||||
wordLang: pangeaMessageEvent.messageDisplayLangCode,
|
||||
fullText: pangeaMessageEvent.messageDisplayText,
|
||||
fullTextLang: pangeaMessageEvent.messageDisplayLangCode,
|
||||
hasInfo: true,
|
||||
room: overLayController.widget.chatController.room,
|
||||
);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: "Error in WordDataCard",
|
||||
s: s,
|
||||
data: {
|
||||
"word": overLayController.targetText,
|
||||
"fullText": pangeaMessageEvent.messageDisplayText,
|
||||
},
|
||||
);
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
case MessageMode.practiceActivity:
|
||||
// If not in the target language show specific messsage
|
||||
if (!overLayController.messageInUserL2) {
|
||||
return MessageDisplayCard(
|
||||
displayText: L10n.of(context)
|
||||
.messageNotInTargetLang, // Pass the display text,
|
||||
);
|
||||
}
|
||||
return PracticeActivityCard(
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
overlayController: overLayController,
|
||||
);
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: "Invalid toolbar mode",
|
||||
s: StackTrace.current,
|
||||
data: {"newMode": overLayController.toolbarMode},
|
||||
);
|
||||
case MessageMode.noneSelected:
|
||||
return const SizedBox();
|
||||
case MessageMode.practiceActivity:
|
||||
case MessageMode.wordZoom:
|
||||
if (overlayController.selectedToken == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return WordZoomWidget(
|
||||
token: overlayController.selectedToken!,
|
||||
messageEvent: overlayController.pangeaMessageEvent!,
|
||||
tts: ttsController,
|
||||
overlayController: overlayController,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (![MessageTypes.Text, MessageTypes.Audio].contains(
|
||||
pangeaMessageEvent.event.messageType,
|
||||
)) {
|
||||
if (overlayController.toolbarMode == MessageMode.noneSelected ||
|
||||
![MessageTypes.Text, MessageTypes.Audio].contains(
|
||||
pangeaMessageEvent.event.messageType,
|
||||
)) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ import 'package:collection/collection.dart';
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/pressable_button.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -16,36 +14,27 @@ import 'package:matrix/matrix.dart';
|
|||
class ToolbarButtons extends StatelessWidget {
|
||||
final Event event;
|
||||
final MessageOverlayController overlayController;
|
||||
final double width;
|
||||
|
||||
const ToolbarButtons({
|
||||
required this.event,
|
||||
required this.overlayController,
|
||||
required this.width,
|
||||
super.key,
|
||||
});
|
||||
|
||||
PangeaMessageEvent? get pangeaMessageEvent =>
|
||||
overlayController.pangeaMessageEvent;
|
||||
int? get activitiesCompleted =>
|
||||
overlayController.pangeaMessageEvent?.numberOfActivitiesCompleted;
|
||||
|
||||
List<MessageMode> get modes => MessageMode.values
|
||||
.where((mode) => mode.shouldShowAsToolbarButton(event))
|
||||
.toList();
|
||||
|
||||
bool get messageInUserL2 =>
|
||||
pangeaMessageEvent?.messageDisplayLangCode ==
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
static const double iconWidth = 36.0;
|
||||
static const buttonSize = 40.0;
|
||||
static const double buttonSize = 40.0;
|
||||
static const double width = 250.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final totallyDone =
|
||||
overlayController.isPracticeComplete || !messageInUserL2;
|
||||
final double barWidth = width - iconWidth;
|
||||
|
||||
if (!overlayController.showToolbarButtons || pangeaMessageEvent == null) {
|
||||
if (!overlayController.showToolbarButtons) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +51,7 @@ class ToolbarButtons extends StatelessWidget {
|
|||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: MessageModeExtension.barAndLockedButtonColor(context),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: iconWidth / 2),
|
||||
),
|
||||
|
|
@ -69,13 +59,12 @@ class ToolbarButtons extends StatelessWidget {
|
|||
duration: FluffyThemes.animationDuration,
|
||||
height: 12,
|
||||
width: overlayController.isPracticeComplete
|
||||
? barWidth
|
||||
: min(
|
||||
barWidth,
|
||||
(barWidth / 3) *
|
||||
pangeaMessageEvent!.numberOfActivitiesCompleted,
|
||||
),
|
||||
color: AppConfig.success,
|
||||
? width
|
||||
: min(width, (width / 2) * activitiesCompleted!),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
color: AppConfig.success,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: iconWidth / 2),
|
||||
),
|
||||
],
|
||||
|
|
@ -86,52 +75,25 @@ class ToolbarButtons extends StatelessWidget {
|
|||
children: modes.mapIndexed((index, mode) {
|
||||
final enabled = mode.isUnlocked(
|
||||
index,
|
||||
pangeaMessageEvent!.numberOfActivitiesCompleted,
|
||||
totallyDone,
|
||||
activitiesCompleted!,
|
||||
overlayController.isPracticeComplete,
|
||||
);
|
||||
final color = mode.iconButtonColor(
|
||||
context,
|
||||
index,
|
||||
overlayController.toolbarMode,
|
||||
pangeaMessageEvent!.numberOfActivitiesCompleted,
|
||||
totallyDone,
|
||||
activitiesCompleted!,
|
||||
overlayController.isPracticeComplete,
|
||||
);
|
||||
return Tooltip(
|
||||
message: mode.tooltip(context),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
PressableButton(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
depressed:
|
||||
!enabled || mode == overlayController.toolbarMode,
|
||||
return mode.showButton
|
||||
? ToolbarButton(
|
||||
mode: mode,
|
||||
overlayController: overlayController,
|
||||
enabled: enabled,
|
||||
buttonSize: buttonSize,
|
||||
color: color,
|
||||
onPressed: enabled
|
||||
? () => overlayController.updateToolbarMode(mode)
|
||||
: null,
|
||||
clickPlayer: overlayController
|
||||
.widget.chatController.choreographer.clickPlayer,
|
||||
child: AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
height: buttonSize,
|
||||
width: buttonSize,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
mode.icon,
|
||||
size: 20,
|
||||
color: mode == overlayController.toolbarMode
|
||||
? Colors.white
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!enabled) const DisabledAnimation(),
|
||||
],
|
||||
),
|
||||
);
|
||||
)
|
||||
: const SizedBox(width: buttonSize);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
|
|
@ -216,3 +178,56 @@ class DisabledAnimationState extends State<DisabledAnimation>
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarButton extends StatelessWidget {
|
||||
final MessageMode mode;
|
||||
final MessageOverlayController overlayController;
|
||||
final bool enabled;
|
||||
final double buttonSize;
|
||||
final Color color;
|
||||
|
||||
const ToolbarButton({
|
||||
required this.mode,
|
||||
required this.overlayController,
|
||||
required this.enabled,
|
||||
required this.buttonSize,
|
||||
required this.color,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: mode.tooltip(context),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
PressableButton(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
depressed: !enabled || mode == overlayController.toolbarMode,
|
||||
color: color,
|
||||
onPressed: enabled
|
||||
? () => overlayController.updateToolbarMode(mode)
|
||||
: null,
|
||||
child: AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
height: buttonSize,
|
||||
width: buttonSize,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
mode.icon,
|
||||
size: 20,
|
||||
color:
|
||||
mode == overlayController.toolbarMode ? Colors.white : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!enabled) const DisabledAnimation(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_text_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@ import 'package:fluffychat/config/app_config.dart';
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/chat_input_row.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OverlayFooter extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
final MessageOverlayController overlayController;
|
||||
|
||||
const OverlayFooter({
|
||||
required this.controller,
|
||||
required this.overlayController,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -34,7 +37,8 @@ class OverlayFooter extends StatelessWidget {
|
|||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
child: ChatInputRow(controller, isOverlay: true),
|
||||
child:
|
||||
ChatInputRow(controller, overlayController: overlayController),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,189 +0,0 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class OverlayMessageText extends StatefulWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
|
||||
const OverlayMessageText({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.overlayController,
|
||||
});
|
||||
|
||||
@override
|
||||
OverlayMessageTextState createState() => OverlayMessageTextState();
|
||||
}
|
||||
|
||||
class OverlayMessageTextState extends State<OverlayMessageText> {
|
||||
List<PangeaToken>? _tokens;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setTokens();
|
||||
}
|
||||
|
||||
Future<void> _setTokens() async {
|
||||
final repEvent = widget.pangeaMessageEvent.messageDisplayRepresentation;
|
||||
if (repEvent != null) {
|
||||
_tokens = repEvent.tokens;
|
||||
_tokens ??= await repEvent.tokensGlobal(
|
||||
widget.pangeaMessageEvent.senderId,
|
||||
widget.pangeaMessageEvent.originServerTs,
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final ownMessage = widget.pangeaMessageEvent.event.senderId ==
|
||||
Matrix.of(context).client.userID;
|
||||
|
||||
final style = TextStyle(
|
||||
color: ownMessage
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurface,
|
||||
height: 1.3,
|
||||
fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor,
|
||||
);
|
||||
|
||||
if (_tokens == null || _tokens!.isEmpty) {
|
||||
return Text(
|
||||
widget.pangeaMessageEvent.event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)),
|
||||
hideReply: true,
|
||||
),
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert the entire message into a list of characters
|
||||
final Characters messageCharacters =
|
||||
widget.pangeaMessageEvent.messageDisplayText.characters;
|
||||
|
||||
// When building token positions, use grapheme cluster indices
|
||||
// We use grapheme cluster indices to avoid splitting emojis and other
|
||||
// complex characters that requires multiple code units.
|
||||
// For instance, the emoji 🇺🇸 is represented by two code units:
|
||||
// - \u{1F1FA}
|
||||
// - \u{1F1F8}
|
||||
final List<TokenPosition> tokenPositions = [];
|
||||
int globalIndex = 0;
|
||||
|
||||
for (int i = 0; i < _tokens!.length; i++) {
|
||||
final token = _tokens![i];
|
||||
final start = token.start;
|
||||
final end = token.end;
|
||||
|
||||
// Calculate the number of grapheme clusters up to the start and end positions
|
||||
final int startIndex = messageCharacters.take(start).length;
|
||||
final int endIndex = messageCharacters.take(end).length;
|
||||
|
||||
if (globalIndex < startIndex) {
|
||||
tokenPositions.add(TokenPosition(start: globalIndex, end: startIndex));
|
||||
}
|
||||
|
||||
tokenPositions.add(
|
||||
TokenPosition(
|
||||
start: startIndex,
|
||||
end: endIndex,
|
||||
tokenIndex: i,
|
||||
token: token,
|
||||
),
|
||||
);
|
||||
globalIndex = endIndex;
|
||||
}
|
||||
|
||||
// debug prints for fixing words sticking together
|
||||
// void printEscapedString(String input) {
|
||||
// // Escaped string using Unicode escape sequences
|
||||
// final String escapedString = input.replaceAllMapped(
|
||||
// RegExp(r'[^\w\s]', unicode: true),
|
||||
// (match) {
|
||||
// final codeUnits = match.group(0)!.runes;
|
||||
// String unicodeEscapes = '';
|
||||
// for (final rune in codeUnits) {
|
||||
// unicodeEscapes += '\\u{${rune.toRadixString(16)}}';
|
||||
// }
|
||||
// return unicodeEscapes;
|
||||
// },
|
||||
// );
|
||||
// print("Escaped String: $escapedString");
|
||||
|
||||
// // Printing each character with its index
|
||||
// int index = 0;
|
||||
// for (final char in input.characters) {
|
||||
// print("Index $index: $char");
|
||||
// index++;
|
||||
// }
|
||||
// }
|
||||
|
||||
//TODO - take out of build function of every message
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
children: tokenPositions.map((tokenPosition) {
|
||||
final substring = messageCharacters
|
||||
.skip(tokenPosition.start)
|
||||
.take(tokenPosition.end - tokenPosition.start)
|
||||
.toString();
|
||||
|
||||
if (tokenPosition.token != null) {
|
||||
final isSelected =
|
||||
widget.overlayController.isTokenSelected(tokenPosition.token!);
|
||||
return TextSpan(
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
debugPrint(
|
||||
'tokenPosition.tokenIndex: ${tokenPosition.tokenIndex}',
|
||||
);
|
||||
widget.overlayController.onClickOverlayMessageToken(
|
||||
tokenPosition.token!,
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
text: substring,
|
||||
style: style.merge(
|
||||
TextStyle(
|
||||
backgroundColor: isSelected
|
||||
? Theme.of(context).brightness == Brightness.light
|
||||
? Colors.black.withOpacity(0.4)
|
||||
: Colors.white.withOpacity(0.4)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return TextSpan(
|
||||
text: substring,
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TokenPosition {
|
||||
final int start;
|
||||
final int end;
|
||||
final PangeaToken? token;
|
||||
final int tokenIndex;
|
||||
|
||||
const TokenPosition({
|
||||
required this.start,
|
||||
required this.end,
|
||||
this.token,
|
||||
this.tokenIndex = -1,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,18 +1,25 @@
|
|||
import 'package:fluffychat/config/app_emojis.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class PangeaReactionsPicker extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
final MessageOverlayController? overlayController;
|
||||
|
||||
const PangeaReactionsPicker(this.controller, {super.key});
|
||||
const PangeaReactionsPicker(
|
||||
this.controller,
|
||||
this.overlayController, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
PangeaToken? get token => overlayController?.selectedToken;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
if (controller.showEmojiPicker) return const SizedBox.shrink();
|
||||
final display = controller.editEvent == null &&
|
||||
controller.replyEvent == null &&
|
||||
|
|
@ -39,6 +46,15 @@ class PangeaReactionsPicker extends StatelessWidget {
|
|||
emojis.remove(event.content.tryGetMap('m.relates_to')!['key']);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
for (final event in allReactionEvents) {
|
||||
try {
|
||||
emojis.remove(
|
||||
event.content.tryGetMap('m.relates_to')!['key'],
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
@ -65,20 +81,6 @@ class PangeaReactionsPicker extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
width: 36,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.onInverseSurface,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.add_outlined),
|
||||
),
|
||||
onTap: () => controller.pickEmojiReactionAction(allReactionEvents),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_tts/flutter_tts.dart' as flutter_tts;
|
||||
import 'package:matrix/matrix_api_lite/utils/logs.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:text_to_speech/text_to_speech.dart';
|
||||
|
||||
class TtsController {
|
||||
|
|
@ -202,32 +201,34 @@ class TtsController {
|
|||
stop();
|
||||
|
||||
Logs().i('Speaking: $text');
|
||||
final result = await (_useAlternativeTTS
|
||||
? _alternativeTTS.speak(text)
|
||||
: _tts.speak(text))
|
||||
.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () {
|
||||
ErrorHandler.logError(
|
||||
e: "Timeout on tts.speak",
|
||||
data: {"text": text},
|
||||
);
|
||||
},
|
||||
final result = await Future(
|
||||
() => (_useAlternativeTTS
|
||||
? _alternativeTTS.speak(text)
|
||||
: _tts.speak(text))
|
||||
.timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () {
|
||||
ErrorHandler.logError(
|
||||
e: "Timeout on tts.speak",
|
||||
data: {"text": text},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
Logs().i('Finished speaking: $text, result: $result');
|
||||
|
||||
// return type is dynamic but apparent its supposed to be 1
|
||||
// https://pub.dev/packages/flutter_tts
|
||||
if (result != 1 && !kIsWeb) {
|
||||
ErrorHandler.logError(
|
||||
m: 'Unexpected result from tts.speak',
|
||||
data: {
|
||||
'result': result,
|
||||
'text': text,
|
||||
},
|
||||
level: SentryLevel.warning,
|
||||
);
|
||||
}
|
||||
// if (result != 1 && !kIsWeb) {
|
||||
// ErrorHandler.logError(
|
||||
// m: 'Unexpected result from tts.speak',
|
||||
// data: {
|
||||
// 'result': result,
|
||||
// 'text': text,
|
||||
// },
|
||||
// level: SentryLevel.warning,
|
||||
// );
|
||||
// }
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
|
|
@ -243,3 +244,7 @@ class TtsController {
|
|||
bool get isLanguageFullySupported =>
|
||||
_availableLangCodes.contains(targetLanguage);
|
||||
}
|
||||
|
||||
extension on (Future,) {
|
||||
timeout(Duration duration, {required Null Function() onTimeout}) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -196,56 +196,59 @@ class WordDataCardView extends StatelessWidget {
|
|||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
maxHeight: AppConfig.toolbarMaxHeight,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
if (controller.widget.choiceFeedback != null)
|
||||
Text(
|
||||
controller.widget.choiceFeedback!,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
if (controller.wordData != null &&
|
||||
controller.wordNetError == null &&
|
||||
controller.activeL1 != null &&
|
||||
controller.activeL2 != null)
|
||||
WordNetInfo(
|
||||
wordData: controller.wordData!,
|
||||
activeL1: controller.activeL1!,
|
||||
activeL2: controller.activeL2!,
|
||||
),
|
||||
if (controller.isLoadingWordNet)
|
||||
const ToolbarContentLoadingIndicator(),
|
||||
const SizedBox(height: 5.0),
|
||||
// if (controller.widget.hasInfo &&
|
||||
// !controller.isLoadingContextualDefinition &&
|
||||
// controller.contextualDefinitionRes == null)
|
||||
// Material(
|
||||
// type: MaterialType.transparency,
|
||||
// child: ListTile(
|
||||
// leading: const BotFace(
|
||||
// width: 40, expression: BotExpression.surprised),
|
||||
// title: Text(L10n.of(context).askPangeaBot),
|
||||
// onTap: controller.handleGetDefinitionButtonPress,
|
||||
// ),
|
||||
// ),
|
||||
if (controller.isLoadingContextualDefinition)
|
||||
const ToolbarContentLoadingIndicator(),
|
||||
if (controller.contextualDefinitionRes != null)
|
||||
Text(
|
||||
controller.contextualDefinitionRes!.text,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (controller.definitionError != null)
|
||||
Text(
|
||||
L10n.of(context).sorryNoResults,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
if (controller.widget.choiceFeedback != null)
|
||||
Text(
|
||||
controller.widget.choiceFeedback!,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
if (controller.wordData != null &&
|
||||
controller.wordNetError == null &&
|
||||
controller.activeL1 != null &&
|
||||
controller.activeL2 != null)
|
||||
WordNetInfo(
|
||||
wordData: controller.wordData!,
|
||||
activeL1: controller.activeL1!,
|
||||
activeL2: controller.activeL2!,
|
||||
),
|
||||
if (controller.isLoadingWordNet)
|
||||
const ToolbarContentLoadingIndicator(),
|
||||
const SizedBox(height: 5.0),
|
||||
// if (controller.widget.hasInfo &&
|
||||
// !controller.isLoadingContextualDefinition &&
|
||||
// controller.contextualDefinitionRes == null)
|
||||
// Material(
|
||||
// type: MaterialType.transparency,
|
||||
// child: ListTile(
|
||||
// leading: const BotFace(
|
||||
// width: 40, expression: BotExpression.surprised),
|
||||
// title: Text(L10n.of(context).askPangeaBot),
|
||||
// onTap: controller.handleGetDefinitionButtonPress,
|
||||
// ),
|
||||
// ),
|
||||
if (controller.isLoadingContextualDefinition)
|
||||
const ToolbarContentLoadingIndicator(),
|
||||
if (controller.contextualDefinitionRes != null)
|
||||
Text(
|
||||
controller.contextualDefinitionRes!.text,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (controller.definitionError != null)
|
||||
Text(
|
||||
L10n.of(context).sorryNoResults,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmojiPracticeButton extends StatefulWidget {
|
||||
final PangeaToken token;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
final String? emoji;
|
||||
final Function(String) setEmoji;
|
||||
|
||||
const EmojiPracticeButton({
|
||||
required this.token,
|
||||
required this.onPressed,
|
||||
this.emoji,
|
||||
required this.setEmoji,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
EmojiPracticeButtonState createState() => EmojiPracticeButtonState();
|
||||
}
|
||||
|
||||
class EmojiPracticeButtonState extends State<EmojiPracticeButton> {
|
||||
@override
|
||||
void didUpdateWidget(covariant EmojiPracticeButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.token != oldWidget.token) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
bool get _canDoActivity {
|
||||
final canDo = widget.token.shouldDoActivity(
|
||||
a: ActivityTypeEnum.emoji,
|
||||
feature: null,
|
||||
tag: null,
|
||||
);
|
||||
return canDo;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final emoji = widget.token.getEmoji();
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: _canDoActivity || emoji != null
|
||||
? IconButton(
|
||||
onPressed: () {
|
||||
widget.onPressed();
|
||||
if (widget.emoji == null && emoji != null) {
|
||||
widget.setEmoji(emoji);
|
||||
}
|
||||
},
|
||||
icon: emoji == null
|
||||
? const Icon(Icons.add_reaction_outlined)
|
||||
: Text(emoji),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
|
|
@ -9,7 +8,9 @@ import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
|||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/grammar/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/word_audio_button.dart';
|
||||
|
|
@ -24,12 +25,14 @@ class MultipleChoiceActivity extends StatefulWidget {
|
|||
final PracticeActivityModel currentActivity;
|
||||
final Event event;
|
||||
final VoidCallback? onError;
|
||||
final MessageOverlayController overlayController;
|
||||
|
||||
const MultipleChoiceActivity({
|
||||
super.key,
|
||||
required this.practiceCardController,
|
||||
required this.currentActivity,
|
||||
required this.event,
|
||||
required this.overlayController,
|
||||
this.onError,
|
||||
});
|
||||
|
||||
|
|
@ -62,7 +65,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
void speakTargetTokens() {
|
||||
if (widget.practiceCardController.currentActivity?.shouldPlayTargetTokens ??
|
||||
false) {
|
||||
widget.practiceCardController.tts.tryToSpeak(
|
||||
tts.tryToSpeak(
|
||||
PangeaToken.reconstructText(
|
||||
widget.practiceCardController.currentActivity!.targetTokens!,
|
||||
),
|
||||
|
|
@ -72,7 +75,8 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
}
|
||||
}
|
||||
|
||||
TtsController get tts => widget.practiceCardController.tts;
|
||||
TtsController get tts =>
|
||||
widget.overlayController.widget.chatController.choreographer.tts;
|
||||
|
||||
void updateChoice(String value, int index) {
|
||||
final bool isCorrect =
|
||||
|
|
@ -145,7 +149,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
if (widget.currentActivity.content.isCorrect(value, index)) {
|
||||
MatrixState.pangeaController.getAnalytics.analyticsStream.stream.first
|
||||
.then((_) {
|
||||
widget.practiceCardController.onActivityFinish();
|
||||
widget.practiceCardController.onActivityFinish(correctAnswer: value);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +160,40 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
}
|
||||
}
|
||||
|
||||
List<Choice> choices(BuildContext context) {
|
||||
final activity = widget.currentActivity.content;
|
||||
final List<Choice> choices = [];
|
||||
for (int i = 0; i < activity.choices.length; i++) {
|
||||
final String value = activity.choices[i];
|
||||
final color = currentRecordModel?.hasTextResponse(value) ?? false
|
||||
? activity.choiceColor(i)
|
||||
: null;
|
||||
final isGold = activity.isCorrect(value, i);
|
||||
choices.add(
|
||||
Choice(
|
||||
text: value,
|
||||
color: color,
|
||||
isGold: isGold,
|
||||
),
|
||||
);
|
||||
}
|
||||
return choices;
|
||||
}
|
||||
|
||||
String _getDisplayCopy(String value) {
|
||||
if (widget.currentActivity.activityType != ActivityTypeEnum.morphId) {
|
||||
return value;
|
||||
}
|
||||
final morphFeature = widget.practiceCardController.widget.morphFeature;
|
||||
if (morphFeature == null) return value;
|
||||
return getGrammarCopy(
|
||||
category: morphFeature,
|
||||
lemma: value,
|
||||
context: context,
|
||||
) ??
|
||||
value;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final PracticeActivityModel practiceActivity = widget.currentActivity;
|
||||
|
|
@ -175,7 +213,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
if (practiceActivity.activityType ==
|
||||
ActivityTypeEnum.wordFocusListening)
|
||||
WordAudioButton(
|
||||
text: practiceActivity.content.answer,
|
||||
text: practiceActivity.content.answers.first,
|
||||
ttsController: tts,
|
||||
eventID: widget.event.eventId,
|
||||
),
|
||||
|
|
@ -184,11 +222,9 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
MessageAudioCard(
|
||||
messageEvent:
|
||||
widget.practiceCardController.widget.pangeaMessageEvent,
|
||||
overlayController:
|
||||
widget.practiceCardController.widget.overlayController,
|
||||
overlayController: widget.overlayController,
|
||||
tts: tts,
|
||||
setIsPlayingAudio: widget.practiceCardController.widget
|
||||
.overlayController.setIsPlayingAudio,
|
||||
setIsPlayingAudio: widget.overlayController.setIsPlayingAudio,
|
||||
onError: widget.onError,
|
||||
),
|
||||
ChoicesArray(
|
||||
|
|
@ -197,37 +233,27 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
originalSpan: "placeholder",
|
||||
onPressed: updateChoice,
|
||||
selectedChoiceIndex: selectedChoiceIndex,
|
||||
choices: practiceActivity.content.choices
|
||||
.mapIndexed(
|
||||
(index, value) => Choice(
|
||||
text: value,
|
||||
color: currentRecordModel?.hasTextResponse(value) ?? false
|
||||
? practiceActivity.content.choiceColor(index)
|
||||
: null,
|
||||
isGold: practiceActivity.content.isCorrect(value, index),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
choices: choices(context),
|
||||
isActive: true,
|
||||
id: currentRecordModel?.hashCode.toString(),
|
||||
tts: practiceActivity.activityType.includeTTSOnClick ? tts : null,
|
||||
enableAudio: !widget
|
||||
.practiceCardController.widget.overlayController.isPlayingAudio,
|
||||
enableAudio: !widget.overlayController.isPlayingAudio,
|
||||
getDisplayCopy: _getDisplayCopy,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: AppConfig.toolbarMinWidth,
|
||||
maxHeight: AppConfig.toolbarMaxHeight,
|
||||
minWidth: AppConfig.toolbarMinWidth,
|
||||
minHeight: AppConfig.toolbarMinHeight,
|
||||
),
|
||||
child:
|
||||
practiceActivity.activityType == ActivityTypeEnum.hiddenWordListening
|
||||
? SingleChildScrollView(child: content)
|
||||
: content,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,40 +2,51 @@ import 'dart:async';
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/practice_activity_generation_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/practice/practice_repo.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/gain_points.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/content_issue_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/no_more_practice_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/word_zoom/word_zoom_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// The wrapper for practice activity content.
|
||||
/// Handles the activities associated with a message,
|
||||
/// their navigation, and the management of completion records
|
||||
class PracticeActivityCard extends StatefulWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final TargetTokensAndActivityType targetTokensAndActivityType;
|
||||
final MessageOverlayController overlayController;
|
||||
final WordZoomWidgetState? wordDetailsController;
|
||||
|
||||
final String? morphFeature;
|
||||
|
||||
//TODO - modifications
|
||||
// 1) Future<PracticeActivityEvent> and Future<PracticeActivityModel> as parameters
|
||||
// 2) onFinish callback as parameter
|
||||
// 3) take out logic fetching activity
|
||||
|
||||
const PracticeActivityCard({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.targetTokensAndActivityType,
|
||||
required this.overlayController,
|
||||
this.morphFeature,
|
||||
this.wordDetailsController,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -44,27 +55,37 @@ class PracticeActivityCard extends StatefulWidget {
|
|||
|
||||
class PracticeActivityCardState extends State<PracticeActivityCard> {
|
||||
PracticeActivityModel? currentActivity;
|
||||
Completer<PracticeActivityEvent?>? currentActivityCompleter;
|
||||
|
||||
PracticeActivityRecordModel? currentCompletionRecord;
|
||||
bool fetchingActivity = false;
|
||||
|
||||
List<PracticeActivityEvent> get practiceActivities =>
|
||||
widget.pangeaMessageEvent.practiceActivities;
|
||||
|
||||
// Used to show an animation when the user completes an activity
|
||||
// while simultaneously fetching a new activity and not showing the loading spinner
|
||||
// until the appropriate time has passed to 'savor the joy'
|
||||
Duration appropriateTimeForJoy = const Duration(milliseconds: 2500);
|
||||
bool savoringTheJoy = false;
|
||||
|
||||
TtsController get tts =>
|
||||
widget.overlayController.widget.chatController.choreographer.tts;
|
||||
PracticeActivityRecordModel? currentCompletionRecord;
|
||||
Completer<PracticeActivityEvent?>? currentActivityCompleter;
|
||||
|
||||
PracticeGenerationController practiceGenerationController =
|
||||
PracticeGenerationController();
|
||||
|
||||
PangeaController get pangeaController => MatrixState.pangeaController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initialize();
|
||||
_fetchActivity();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(PracticeActivityCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.targetTokensAndActivityType !=
|
||||
widget.targetTokensAndActivityType ||
|
||||
oldWidget.morphFeature != widget.morphFeature) {
|
||||
_fetchActivity();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
practiceGenerationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateFetchingActivity(bool value) {
|
||||
|
|
@ -72,129 +93,120 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
if (mounted) setState(() => fetchingActivity = value);
|
||||
}
|
||||
|
||||
void _setPracticeActivity(PracticeActivityModel? activity) {
|
||||
//set elsewhere but just in case
|
||||
fetchingActivity = false;
|
||||
|
||||
currentActivity = activity;
|
||||
|
||||
if (activity == null) {
|
||||
widget.overlayController.exitPracticeFlow();
|
||||
Future<void> _fetchActivity({
|
||||
ActivityQualityFeedback? activityFeedback,
|
||||
}) async {
|
||||
if (!mounted ||
|
||||
!pangeaController.languageController.languagesSet ||
|
||||
widget.overlayController.messageAnalyticsEntry == null) {
|
||||
debugger(when: kDebugMode);
|
||||
_updateFetchingActivity(false);
|
||||
return;
|
||||
}
|
||||
|
||||
//make new completion record
|
||||
currentCompletionRecord = PracticeActivityRecordModel(
|
||||
question: activity.question,
|
||||
);
|
||||
|
||||
widget.overlayController.setSelectedSpan(activity);
|
||||
}
|
||||
|
||||
/// Get an existing activity if there is one.
|
||||
/// If not, get a new activity from the server.
|
||||
Future<void> initialize() async {
|
||||
_setPracticeActivity(
|
||||
await _fetchActivity(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<PracticeActivityModel?> _fetchActivity({
|
||||
ActivityQualityFeedback? activityFeedback,
|
||||
}) async {
|
||||
try {
|
||||
debugPrint('Fetching activity');
|
||||
_updateFetchingActivity(true);
|
||||
|
||||
// target tokens can be empty if activities have been completed for each
|
||||
// it's set on initialization and then removed when each activity is completed
|
||||
if (!mounted ||
|
||||
!pangeaController.languageController.languagesSet ||
|
||||
widget.overlayController.messageAnalyticsEntry == null) {
|
||||
debugger(when: kDebugMode);
|
||||
_updateFetchingActivity(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
final nextActivitySpecs =
|
||||
widget.overlayController.messageAnalyticsEntry?.nextActivity;
|
||||
// the client is going to be choosing the next activity now
|
||||
// if nothing is set then it must be done with practice
|
||||
if (nextActivitySpecs == null) {
|
||||
debugPrint("No next activity set, exiting practice flow");
|
||||
_updateFetchingActivity(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
// check if we already have an activity matching the specs
|
||||
final existingActivity = practiceActivities.firstWhereOrNull(
|
||||
(activity) =>
|
||||
nextActivitySpecs.matchesActivity(activity.practiceActivity),
|
||||
);
|
||||
if (existingActivity != null) {
|
||||
debugPrint('found existing activity');
|
||||
_updateFetchingActivity(false);
|
||||
existingActivity.practiceActivity.targetTokens =
|
||||
nextActivitySpecs.tokens;
|
||||
currentActivityCompleter = Completer();
|
||||
currentActivityCompleter!.complete(existingActivity);
|
||||
return existingActivity.practiceActivity;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
"client requesting ${nextActivitySpecs.activityType.string} for: ${nextActivitySpecs.tokens.map((t) => "construct: ${t.lemma.text}:${t.pos} points: ${t.vocabConstruct.points}").join(' ')}",
|
||||
final activity = await _fetchActivityModel(
|
||||
activityFeedback: activityFeedback,
|
||||
);
|
||||
|
||||
// debugger(
|
||||
// when: kDebugMode &&
|
||||
// nextActivitySpecs.tokens
|
||||
// .map((a) => a.vocabConstruct.points)
|
||||
// .reduce((a, b) => a + b) >
|
||||
// 30 &&
|
||||
// nextActivitySpecs.activityType == ActivityTypeEnum.wordMeaning,
|
||||
// );
|
||||
|
||||
final PracticeActivityModelResponse? activityResponse =
|
||||
await pangeaController.practiceGenerationController
|
||||
.getPracticeActivity(
|
||||
MessageActivityRequest(
|
||||
userL1: pangeaController.languageController.userL1!.langCode,
|
||||
userL2: pangeaController.languageController.userL2!.langCode,
|
||||
messageText: widget.pangeaMessageEvent.messageDisplayText,
|
||||
messageTokens: widget.overlayController.tokens!,
|
||||
activityQualityFeedback: activityFeedback,
|
||||
targetTokens: nextActivitySpecs.tokens,
|
||||
targetType: nextActivitySpecs.activityType,
|
||||
),
|
||||
widget.pangeaMessageEvent,
|
||||
);
|
||||
|
||||
currentActivityCompleter = activityResponse?.eventCompleter;
|
||||
_updateFetchingActivity(false);
|
||||
|
||||
if (activityResponse == null || activityResponse.activity == null) {
|
||||
debugPrint('No activity found');
|
||||
return null;
|
||||
currentActivity = activity;
|
||||
if (activity == null) {
|
||||
widget.overlayController.exitPracticeFlow();
|
||||
return;
|
||||
}
|
||||
|
||||
activityResponse.activity!.targetTokens = nextActivitySpecs.tokens;
|
||||
|
||||
return activityResponse.activity;
|
||||
currentCompletionRecord = PracticeActivityRecordModel(
|
||||
question: activity.question,
|
||||
);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
m: 'Failed to get new activity',
|
||||
data: {
|
||||
'activity': currentActivity,
|
||||
'record': currentCompletionRecord,
|
||||
'activity': currentActivity?.toJson(),
|
||||
'record': currentCompletionRecord?.toJson(),
|
||||
'targetTokens': widget.targetTokensAndActivityType.tokens
|
||||
.map((token) => token.toJson())
|
||||
.toList(),
|
||||
'activityType': widget.targetTokensAndActivityType.activityType,
|
||||
'morphFeature': widget.morphFeature,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
debugger(when: kDebugMode);
|
||||
} finally {
|
||||
_updateFetchingActivity(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<PracticeActivityModel?> _fetchActivityModel({
|
||||
ActivityQualityFeedback? activityFeedback,
|
||||
}) async {
|
||||
debugPrint(
|
||||
"fetching activity model of type: ${widget.targetTokensAndActivityType.activityType}",
|
||||
);
|
||||
// check if we already have an activity matching the specs
|
||||
final tokens = widget.targetTokensAndActivityType.tokens;
|
||||
final type = widget.targetTokensAndActivityType.activityType;
|
||||
|
||||
final existingActivity =
|
||||
widget.pangeaMessageEvent.practiceActivities.firstWhereOrNull(
|
||||
(activity) {
|
||||
final sameActivity = activity.practiceActivity.targetTokens != null &&
|
||||
activity.practiceActivity.activityType == type &&
|
||||
activity.practiceActivity.targetTokens!
|
||||
.map((t) => t.vocabConstructID.string)
|
||||
.toSet()
|
||||
.containsAll(
|
||||
tokens.map((t) => t.vocabConstructID.string).toSet(),
|
||||
);
|
||||
if (type != ActivityTypeEnum.morphId || sameActivity == false) {
|
||||
return sameActivity;
|
||||
}
|
||||
|
||||
return widget.morphFeature ==
|
||||
activity.practiceActivity.tgtConstructs
|
||||
.firstWhereOrNull(
|
||||
(c) => c.type == ConstructTypeEnum.morph,
|
||||
)
|
||||
?.category;
|
||||
},
|
||||
);
|
||||
|
||||
if (existingActivity != null &&
|
||||
existingActivity.practiceActivity.content.answers.isNotEmpty &&
|
||||
!(existingActivity.practiceActivity.content.answers.length == 1 &&
|
||||
existingActivity.practiceActivity.content.answers.first.isEmpty)) {
|
||||
currentActivityCompleter = Completer();
|
||||
currentActivityCompleter!.complete(existingActivity);
|
||||
existingActivity.practiceActivity.targetTokens = tokens;
|
||||
return existingActivity.practiceActivity;
|
||||
}
|
||||
|
||||
final req = MessageActivityRequest(
|
||||
userL1: MatrixState.pangeaController.languageController.userL1!.langCode,
|
||||
userL2: MatrixState.pangeaController.languageController.userL2!.langCode,
|
||||
messageText: widget.pangeaMessageEvent.messageDisplayText,
|
||||
messageTokens: widget.overlayController.tokens!,
|
||||
activityQualityFeedback: activityFeedback,
|
||||
targetTokens: tokens,
|
||||
targetType: type,
|
||||
targetMorphFeature: widget.morphFeature,
|
||||
);
|
||||
|
||||
final PracticeActivityModelResponse activityResponse =
|
||||
await practiceGenerationController.getPracticeActivity(
|
||||
req,
|
||||
widget.pangeaMessageEvent,
|
||||
);
|
||||
|
||||
if (activityResponse.activity == null) return null;
|
||||
|
||||
currentActivityCompleter = activityResponse.eventCompleter;
|
||||
activityResponse.activity!.targetTokens = tokens;
|
||||
return activityResponse.activity;
|
||||
}
|
||||
|
||||
ConstructUseMetaData get metadata => ConstructUseMetaData(
|
||||
eventId: widget.pangeaMessageEvent.eventId,
|
||||
roomId: widget.pangeaMessageEvent.room.id,
|
||||
|
|
@ -206,9 +218,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
debugger(when: savoringTheJoy && kDebugMode);
|
||||
|
||||
if (mounted) setState(() => savoringTheJoy = true);
|
||||
|
||||
await Future.delayed(appropriateTimeForJoy);
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (mounted) setState(() => savoringTheJoy = false);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -228,30 +238,23 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
/// Saves the completion record and sends it to the server.
|
||||
/// Fetches a new activity if there are any left to complete.
|
||||
/// Exits the practice flow if there are no more activities.
|
||||
void onActivityFinish() async {
|
||||
void onActivityFinish({String? correctAnswer}) async {
|
||||
try {
|
||||
if (currentCompletionRecord == null || currentActivity == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
widget.overlayController.messageAnalyticsEntry!
|
||||
.onActivityComplete(currentActivity!);
|
||||
|
||||
widget.overlayController.onActivityFinish();
|
||||
|
||||
pangeaController.activityRecordController.completeActivity(
|
||||
widget.pangeaMessageEvent.eventId,
|
||||
);
|
||||
|
||||
// wait for the joy to be savored before resolving the activity
|
||||
// and setting it to replace the previous activity
|
||||
final Iterable<dynamic> result = await Future.wait([
|
||||
_savorTheJoy(),
|
||||
_fetchActivity(),
|
||||
]);
|
||||
|
||||
_setPracticeActivity(result.last as PracticeActivityModel?);
|
||||
await _savorTheJoy();
|
||||
widget.wordDetailsController?.onActivityFinish(
|
||||
activityType: currentActivity!.activityType,
|
||||
correctAnswer: correctAnswer,
|
||||
);
|
||||
} catch (e, s) {
|
||||
_onError();
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -268,79 +271,14 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
|
||||
void _onError() {
|
||||
widget.overlayController.messageAnalyticsEntry?.revealAllTokens();
|
||||
_setPracticeActivity(null);
|
||||
currentActivity = null;
|
||||
widget.overlayController.exitPracticeFlow();
|
||||
}
|
||||
|
||||
bool _isActivityRedaction(EventUpdate update, String activityId) {
|
||||
return update.content.containsKey('type') &&
|
||||
update.content['type'] == 'm.room.redaction' &&
|
||||
update.content.containsKey('content') &&
|
||||
update.content['content']['redacts'] == activityId;
|
||||
}
|
||||
|
||||
/// clear the current activity, record, and selection
|
||||
/// fetch a new activity, including the offending activity in the request
|
||||
Future<void> submitFeedback(String feedback) async {
|
||||
if (currentActivity == null || currentCompletionRecord == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentActivityCompleter != null) {
|
||||
final activityEvent = await currentActivityCompleter!.future;
|
||||
if (activityEvent != null) {
|
||||
await activityEvent.event.redactEvent(reason: feedback);
|
||||
final eventID = activityEvent.event.eventId;
|
||||
await activityEvent.event.room.client.onEvent.stream
|
||||
.firstWhere(
|
||||
(update) => _isActivityRedaction(update, eventID),
|
||||
)
|
||||
.timeout(const Duration(milliseconds: 2500));
|
||||
}
|
||||
} else {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: Exception('No completer found for current activity'),
|
||||
data: {
|
||||
'activity': currentActivity,
|
||||
'record': currentCompletionRecord,
|
||||
'feedback': feedback,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_fetchActivity(
|
||||
activityFeedback: ActivityQualityFeedback(
|
||||
feedbackText: feedback,
|
||||
badActivity: currentActivity!,
|
||||
),
|
||||
).then((activity) {
|
||||
_setPracticeActivity(activity);
|
||||
}).catchError((onError) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: onError,
|
||||
m: 'Failed to get new activity',
|
||||
data: {
|
||||
'activity': currentActivity,
|
||||
'record': currentCompletionRecord,
|
||||
},
|
||||
);
|
||||
|
||||
// clear the current activity and record
|
||||
currentActivity = null;
|
||||
currentCompletionRecord = null;
|
||||
|
||||
widget.overlayController.exitPracticeFlow();
|
||||
});
|
||||
}
|
||||
|
||||
PangeaController get pangeaController => MatrixState.pangeaController;
|
||||
|
||||
/// The widget that displays the current activity.
|
||||
/// If there is no current activity, the widget returns a sizedbox with a height of 80.
|
||||
/// If the activity type is multiple choice, the widget returns a MultipleChoiceActivity.
|
||||
/// If the activity type is unknown, the widget logs an error and returns a text widget with an error message.
|
||||
// /// The widget that displays the current activity.
|
||||
// /// If there is no current activity, the widget returns a sizedbox with a height of 80.
|
||||
// /// If the activity type is multiple choice, the widget returns a MultipleChoiceActivity.
|
||||
// /// If the activity type is unknown, the widget logs an error and returns a text widget with an error message.
|
||||
Widget? get activityWidget {
|
||||
switch (currentActivity?.activityType) {
|
||||
case null:
|
||||
|
|
@ -348,11 +286,15 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
case ActivityTypeEnum.wordFocusListening:
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
case ActivityTypeEnum.emoji:
|
||||
case ActivityTypeEnum.morphId:
|
||||
return MultipleChoiceActivity(
|
||||
practiceCardController: this,
|
||||
currentActivity: currentActivity!,
|
||||
event: widget.pangeaMessageEvent.event,
|
||||
onError: _onError,
|
||||
overlayController: widget.overlayController,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -360,7 +302,8 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!fetchingActivity && currentActivity == null) {
|
||||
return const GamifiedTextWidget();
|
||||
debugPrint("don't think we should be here");
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
|
|
@ -379,14 +322,14 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
const ToolbarContentLoadingIndicator(),
|
||||
],
|
||||
// Flag button in the top right corner
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: ContentIssueButton(
|
||||
isActive: currentActivity != null,
|
||||
submitFeedback: submitFeedback,
|
||||
),
|
||||
),
|
||||
// Positioned(
|
||||
// top: 0,
|
||||
// right: 0,
|
||||
// child: ContentIssueButton(
|
||||
// isActive: currentActivity != null,
|
||||
// submitFeedback: submitFeedback,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class WordTextWithAudioButton extends StatefulWidget {
|
||||
final String text;
|
||||
final TtsController ttsController;
|
||||
final String eventID;
|
||||
|
||||
const WordTextWithAudioButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.ttsController,
|
||||
required this.eventID,
|
||||
});
|
||||
|
||||
@override
|
||||
WordAudioButtonState createState() => WordAudioButtonState();
|
||||
}
|
||||
|
||||
class WordAudioButtonState extends State<WordTextWithAudioButton> {
|
||||
bool _isPlaying = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (event) => setState(() {}),
|
||||
onExit: (event) => setState(() {}),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
if (_isPlaying) {
|
||||
await widget.ttsController.stop();
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = false);
|
||||
}
|
||||
} else {
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = true);
|
||||
}
|
||||
try {
|
||||
await widget.ttsController.tryToSpeak(
|
||||
widget.text,
|
||||
context,
|
||||
widget.eventID,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"text": widget.text,
|
||||
"eventID": widget.eventID,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
boxShadow: _isHovering
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
blurRadius: 4,
|
||||
spreadRadius: 1,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.text,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: _isPlaying
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
_isPlaying ? Icons.play_arrow : Icons.play_arrow_outlined,
|
||||
size: Theme.of(context).textTheme.bodyMedium?.fontSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final bool _isHovering = false;
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class ContextualTranslationWidget extends StatefulWidget {
|
||||
final PangeaToken token;
|
||||
final String fullText;
|
||||
final String langCode;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
final String? definition;
|
||||
final Function(String) setDefinition;
|
||||
|
||||
const ContextualTranslationWidget({
|
||||
super.key,
|
||||
required this.token,
|
||||
required this.fullText,
|
||||
required this.langCode,
|
||||
required this.onPressed,
|
||||
required this.setDefinition,
|
||||
this.definition,
|
||||
});
|
||||
|
||||
@override
|
||||
ContextualTranslationWidgetState createState() =>
|
||||
ContextualTranslationWidgetState();
|
||||
}
|
||||
|
||||
class ContextualTranslationWidgetState
|
||||
extends State<ContextualTranslationWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.definition == null) {
|
||||
_fetchDefinition();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ContextualTranslationWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.token != widget.token && widget.definition == null) {
|
||||
_fetchDefinition();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchDefinition() async {
|
||||
final FullTextTranslationResponseModel response =
|
||||
await FullTextTranslationRepo.translate(
|
||||
accessToken: MatrixState.pangeaController.userController.accessToken,
|
||||
request: FullTextTranslationRequestModel(
|
||||
text: widget.fullText,
|
||||
tgtLang:
|
||||
MatrixState.pangeaController.languageController.userL1?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
userL2:
|
||||
MatrixState.pangeaController.languageController.userL2?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
userL1:
|
||||
MatrixState.pangeaController.languageController.userL1?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
offset: widget.token.text.offset,
|
||||
length: widget.token.text.length,
|
||||
deepL: false,
|
||||
),
|
||||
);
|
||||
widget.setDefinition(response.bestTranslation);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
width: 60,
|
||||
child: IconButton(
|
||||
iconSize: 30,
|
||||
onPressed: widget.onPressed,
|
||||
icon: const Icon(Symbols.dictionary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
70
lib/pangea/widgets/word_zoom/lemma_definition_widget.dart
Normal file
70
lib/pangea/widgets/word_zoom/lemma_definition_widget.dart
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/lemma_definition_repo.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LemmaDefinitionWidget extends StatefulWidget {
|
||||
final PangeaToken token;
|
||||
final String tokenLang;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const LemmaDefinitionWidget({
|
||||
super.key,
|
||||
required this.token,
|
||||
required this.tokenLang,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
LemmaDefinitionWidgetState createState() => LemmaDefinitionWidgetState();
|
||||
}
|
||||
|
||||
class LemmaDefinitionWidgetState extends State<LemmaDefinitionWidget> {
|
||||
late Future<String> _definition;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_definition = _fetchDefinition();
|
||||
}
|
||||
|
||||
Future<String> _fetchDefinition() async {
|
||||
if (widget.token.shouldDoPosActivity) {
|
||||
return '?';
|
||||
} else {
|
||||
final res = await LemmaDictionaryRepo.get(
|
||||
LemmaDefinitionRequest(
|
||||
lemma: widget.token.lemma.text,
|
||||
partOfSpeech: widget.token.pos,
|
||||
lemmaLang: widget.tokenLang,
|
||||
userL1: MatrixState
|
||||
.pangeaController.languageController.userL1?.langCode ??
|
||||
LanguageKeys.defaultLanguage,
|
||||
),
|
||||
);
|
||||
return res.definition;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<String>(
|
||||
future: _definition,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const CircularProgressIndicator();
|
||||
} else if (snapshot.hasError) {
|
||||
// TODO better error widget
|
||||
return Text('Error: ${snapshot.error}');
|
||||
} else {
|
||||
return ActionChip(
|
||||
avatar: const Icon(Icons.book),
|
||||
label: Text(snapshot.data ?? 'No definition found'),
|
||||
onPressed: widget.onPressed,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
35
lib/pangea/widgets/word_zoom/lemma_widget.dart
Normal file
35
lib/pangea/widgets/word_zoom/lemma_widget.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LemmaWidget extends StatelessWidget {
|
||||
final PangeaToken token;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
final String? lemma;
|
||||
final Function(String) setLemma;
|
||||
|
||||
const LemmaWidget({
|
||||
super.key,
|
||||
required this.token,
|
||||
required this.onPressed,
|
||||
this.lemma,
|
||||
required this.setLemma,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
onPressed();
|
||||
if (lemma == null) {
|
||||
setLemma(token.lemma.text);
|
||||
}
|
||||
},
|
||||
icon: Text(token.xpEmoji),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
236
lib/pangea/widgets/word_zoom/morphological_widget.dart
Normal file
236
lib/pangea/widgets/word_zoom/morphological_widget.dart
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/analytics/morph_categories_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class ActivityMorph {
|
||||
final String morphFeature;
|
||||
final String morphTag;
|
||||
bool revealed;
|
||||
|
||||
ActivityMorph({
|
||||
required this.morphFeature,
|
||||
required this.morphTag,
|
||||
required this.revealed,
|
||||
});
|
||||
}
|
||||
|
||||
class MorphologicalListWidget extends StatefulWidget {
|
||||
final PangeaToken token;
|
||||
final String? selectedMorphFeature;
|
||||
final Function(String?) setMorphFeature;
|
||||
final int completedActivities;
|
||||
|
||||
const MorphologicalListWidget({
|
||||
super.key,
|
||||
required this.selectedMorphFeature,
|
||||
required this.token,
|
||||
required this.setMorphFeature,
|
||||
required this.completedActivities,
|
||||
});
|
||||
|
||||
@override
|
||||
MorphologicalListWidgetState createState() => MorphologicalListWidgetState();
|
||||
}
|
||||
|
||||
class MorphologicalListWidgetState extends State<MorphologicalListWidget> {
|
||||
// TODO: make this is a list of morphological features icons based on MorphActivityGenerator.getSequence
|
||||
// For each item in the sequence,
|
||||
// if shouldDoActivity is true, show the template icon then stop
|
||||
// if shouldDoActivity is false, show the actual icon and value then go to the next item
|
||||
|
||||
final List<ActivityMorph> _morphs = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setMorphs();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MorphologicalListWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.token != oldWidget.token) {
|
||||
_setMorphs();
|
||||
}
|
||||
|
||||
if (widget.completedActivities != oldWidget.completedActivities &&
|
||||
oldWidget.selectedMorphFeature != null) {
|
||||
final oldSelectedMorphIndex = _morphs.indexWhere(
|
||||
(morph) => morph.morphFeature == oldWidget.selectedMorphFeature,
|
||||
);
|
||||
if (oldSelectedMorphIndex != -1 &&
|
||||
oldSelectedMorphIndex < _morphs.length) {
|
||||
setState(
|
||||
() => _morphs[oldSelectedMorphIndex].revealed = true,
|
||||
);
|
||||
|
||||
final nextIndex = oldSelectedMorphIndex + 1;
|
||||
if (nextIndex < _morphs.length) {
|
||||
widget.setMorphFeature(_morphs[nextIndex].morphFeature);
|
||||
} else {
|
||||
widget.setMorphFeature(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setMorphs() async {
|
||||
_morphs.clear();
|
||||
final morphEntries = widget.token.morph.entries.toList();
|
||||
for (final morphEntry in morphEntries) {
|
||||
final morphFeature = morphEntry.key;
|
||||
final morphTag = morphEntry.value;
|
||||
final shouldDoActivity = widget.token.shouldDoMorphActivity(morphFeature);
|
||||
final canGenerateDistractors = await widget.token.canGenerateDistractors(
|
||||
ActivityTypeEnum.morphId,
|
||||
morphFeature: morphFeature,
|
||||
morphTag: morphTag,
|
||||
);
|
||||
_morphs.add(
|
||||
ActivityMorph(
|
||||
morphFeature: morphFeature,
|
||||
morphTag: morphTag,
|
||||
revealed: !shouldDoActivity || !canGenerateDistractors,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_morphs.sort((a, b) {
|
||||
if (a.revealed && !b.revealed) {
|
||||
return -1;
|
||||
} else if (!a.revealed && b.revealed) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.morphFeature.toLowerCase() == "pos") {
|
||||
return -1;
|
||||
} else if (b.morphFeature.toLowerCase() == "pos") {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.morphFeature.compareTo(b.morphFeature);
|
||||
});
|
||||
}
|
||||
|
||||
List<ActivityMorph> get _visibleMorphs {
|
||||
final lastRevealedIndex = _morphs.lastIndexWhere((morph) => morph.revealed);
|
||||
|
||||
// if none of the morphs are revealed, show only the first one
|
||||
if (lastRevealedIndex == -1) {
|
||||
return _morphs.take(1).toList();
|
||||
}
|
||||
|
||||
// show all the revealed morphs + the first one with an activity
|
||||
return _morphs.take(lastRevealedIndex + 2).toList();
|
||||
}
|
||||
|
||||
// TODO Use the icons that Khue is creating
|
||||
IconData _getIconForMorphFeature(String feature) {
|
||||
// Define a function to get the icon based on the universal dependency morphological feature (key)
|
||||
switch (feature.toLowerCase()) {
|
||||
case 'number':
|
||||
// google material 123 icon
|
||||
return Icons.format_list_numbered;
|
||||
case 'gender':
|
||||
return Icons.wc;
|
||||
case 'tense':
|
||||
return Icons.access_time;
|
||||
case 'mood':
|
||||
return Icons.mood;
|
||||
case 'person':
|
||||
return Icons.person;
|
||||
case 'case':
|
||||
return Icons.format_list_bulleted;
|
||||
case 'degree':
|
||||
return Icons.trending_up;
|
||||
case 'verbform':
|
||||
return Icons.text_format;
|
||||
case 'voice':
|
||||
return Icons.record_voice_over;
|
||||
case 'aspect':
|
||||
return Icons.aspect_ratio;
|
||||
case 'prontype':
|
||||
return Icons.text_fields;
|
||||
case 'numtype':
|
||||
return Icons.format_list_numbered;
|
||||
case 'poss':
|
||||
return Icons.account_balance;
|
||||
case 'reflex':
|
||||
return Icons.refresh;
|
||||
case 'foreign':
|
||||
return Icons.language;
|
||||
case 'abbr':
|
||||
return Icons.text_format;
|
||||
case 'nountype':
|
||||
return Symbols.abc;
|
||||
case 'pos':
|
||||
return Symbols.toys_and_games;
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
return Icons.help_outline;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Wrap(
|
||||
children: _visibleMorphs.map((morph) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: MorphologicalActivityButton(
|
||||
onPressed: widget.setMorphFeature,
|
||||
morphCategory: morph.morphFeature,
|
||||
icon: _getIconForMorphFeature(morph.morphFeature),
|
||||
isUnlocked: morph.revealed,
|
||||
isSelected: widget.selectedMorphFeature == morph.morphFeature,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MorphologicalActivityButton extends StatelessWidget {
|
||||
final Function(String) onPressed;
|
||||
final String morphCategory;
|
||||
final IconData icon;
|
||||
|
||||
final bool isUnlocked;
|
||||
final bool isSelected;
|
||||
|
||||
const MorphologicalActivityButton({
|
||||
required this.onPressed,
|
||||
required this.morphCategory,
|
||||
required this.icon,
|
||||
this.isUnlocked = true,
|
||||
this.isSelected = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Tooltip(
|
||||
message: getMorphologicalCategoryCopy(
|
||||
morphCategory,
|
||||
context,
|
||||
),
|
||||
child: Opacity(
|
||||
opacity: (isUnlocked && !isSelected) ? 0.75 : 1,
|
||||
child: IconButton(
|
||||
onPressed: () => onPressed(morphCategory),
|
||||
icon: Icon(icon),
|
||||
color: isSelected ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
51
lib/pangea/widgets/word_zoom/part_of_speech_widget.dart
Normal file
51
lib/pangea/widgets/word_zoom/part_of_speech_widget.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
|
||||
class PartOfSpeechWidget extends StatefulWidget {
|
||||
final PangeaToken token;
|
||||
|
||||
const PartOfSpeechWidget({super.key, required this.token});
|
||||
|
||||
@override
|
||||
_PartOfSpeechWidgetState createState() => _PartOfSpeechWidgetState();
|
||||
}
|
||||
|
||||
class _PartOfSpeechWidgetState extends State<PartOfSpeechWidget> {
|
||||
late Future<String> _partOfSpeech;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_partOfSpeech = _fetchPartOfSpeech();
|
||||
}
|
||||
|
||||
Future<String> _fetchPartOfSpeech() async {
|
||||
if (widget.token.shouldDoPosActivity) {
|
||||
return '?';
|
||||
} else {
|
||||
return widget.token.pos;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<String>(
|
||||
future: _partOfSpeech,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const CircularProgressIndicator();
|
||||
} else if (snapshot.hasError) {
|
||||
return Text('Error: ${snapshot.error}');
|
||||
} else {
|
||||
return ActionChip(
|
||||
avatar: const Icon(Icons.label),
|
||||
label: Text(snapshot.data ?? 'No part of speech found'),
|
||||
onPressed: () {
|
||||
// Handle chip click
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
289
lib/pangea/widgets/word_zoom/word_zoom_widget.dart
Normal file
289
lib/pangea/widgets/word_zoom/word_zoom_widget.dart
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/grammar/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/emoji_practice_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/word_text_with_audio_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/word_zoom/contextual_translation_widget.dart';
|
||||
import 'package:fluffychat/pangea/widgets/word_zoom/lemma_widget.dart';
|
||||
import 'package:fluffychat/pangea/widgets/word_zoom/morphological_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum WordZoomSelection {
|
||||
translation,
|
||||
emoji,
|
||||
}
|
||||
|
||||
class WordZoomWidget extends StatefulWidget {
|
||||
final PangeaToken token;
|
||||
final PangeaMessageEvent messageEvent;
|
||||
final TtsController tts;
|
||||
final MessageOverlayController overlayController;
|
||||
|
||||
const WordZoomWidget({
|
||||
super.key,
|
||||
required this.token,
|
||||
required this.messageEvent,
|
||||
required this.tts,
|
||||
required this.overlayController,
|
||||
});
|
||||
|
||||
@override
|
||||
WordZoomWidgetState createState() => WordZoomWidgetState();
|
||||
}
|
||||
|
||||
class WordZoomWidgetState extends State<WordZoomWidget> {
|
||||
ActivityTypeEnum? _activityType;
|
||||
|
||||
// morphological activities
|
||||
String? _selectedMorphFeature;
|
||||
|
||||
/// used to trigger a rebuild of the morph activity
|
||||
/// button when a morph activity is completed
|
||||
int completedMorphActivities = 0;
|
||||
|
||||
// defintion activities
|
||||
String? _definition;
|
||||
|
||||
// lemma activities
|
||||
String? _lemma;
|
||||
|
||||
// emoji activities
|
||||
String? _emoji;
|
||||
|
||||
// whether activity type can be generated
|
||||
Map<ActivityTypeEnum, bool> canGenerateActivity = {
|
||||
ActivityTypeEnum.morphId: true,
|
||||
ActivityTypeEnum.wordMeaning: true,
|
||||
ActivityTypeEnum.lemmaId: false,
|
||||
ActivityTypeEnum.emoji: true,
|
||||
};
|
||||
|
||||
Future<void> _initCanGenerateActivity() async {
|
||||
widget.token.canGenerateDistractors(ActivityTypeEnum.lemmaId).then((value) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
canGenerateActivity[ActivityTypeEnum.lemmaId] = value;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initCanGenerateActivity();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant WordZoomWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.token != oldWidget.token) {
|
||||
_clean();
|
||||
_initCanGenerateActivity();
|
||||
}
|
||||
}
|
||||
|
||||
bool _showActivityCard(ActivityTypeEnum? activityType) {
|
||||
if (activityType == null) return false;
|
||||
final shouldDo = widget.token.shouldDoActivity(
|
||||
a: activityType,
|
||||
feature: _selectedMorphFeature,
|
||||
tag: _selectedMorphFeature == null
|
||||
? null
|
||||
: widget.token.morph[_selectedMorphFeature],
|
||||
);
|
||||
|
||||
return canGenerateActivity[activityType]! && shouldDo;
|
||||
}
|
||||
|
||||
void _clean() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_activityType = null;
|
||||
_selectedMorphFeature = null;
|
||||
_definition = null;
|
||||
_lemma = null;
|
||||
_emoji = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _setSelectedMorphFeature(String? feature) {
|
||||
_selectedMorphFeature = _selectedMorphFeature == feature ? null : feature;
|
||||
_setActivityType(
|
||||
_selectedMorphFeature == null ? null : ActivityTypeEnum.morphId,
|
||||
);
|
||||
}
|
||||
|
||||
void _setActivityType(ActivityTypeEnum? activityType) {
|
||||
if (mounted) setState(() => _activityType = activityType);
|
||||
}
|
||||
|
||||
void _setDefinition(String definition) {
|
||||
if (mounted) setState(() => _definition = definition);
|
||||
}
|
||||
|
||||
void _setLemma(String lemma) {
|
||||
if (mounted) setState(() => _lemma = lemma);
|
||||
}
|
||||
|
||||
void _setEmoji(String emoji) {
|
||||
if (mounted) setState(() => _emoji = emoji);
|
||||
}
|
||||
|
||||
void onActivityFinish({
|
||||
required ActivityTypeEnum activityType,
|
||||
String? correctAnswer,
|
||||
}) {
|
||||
switch (activityType) {
|
||||
case ActivityTypeEnum.morphId:
|
||||
if (mounted) setState(() => completedMorphActivities++);
|
||||
break;
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
if (correctAnswer == null) return;
|
||||
_setDefinition(correctAnswer);
|
||||
break;
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
if (correctAnswer == null) return;
|
||||
_setLemma(correctAnswer);
|
||||
break;
|
||||
case ActivityTypeEnum.emoji:
|
||||
if (correctAnswer == null) return;
|
||||
widget.token
|
||||
.setEmoji(correctAnswer)
|
||||
.then((_) => _setEmoji(correctAnswer));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Widget get _activityAnswer {
|
||||
switch (_activityType) {
|
||||
case ActivityTypeEnum.morphId:
|
||||
if (_selectedMorphFeature == null) {
|
||||
return const Text("There should be a selected morph feature");
|
||||
}
|
||||
final String morphTag = widget.token.morph[_selectedMorphFeature!];
|
||||
final copy = getGrammarCopy(
|
||||
category: _selectedMorphFeature!,
|
||||
lemma: morphTag,
|
||||
context: context,
|
||||
);
|
||||
return Text(copy ?? morphTag);
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return _definition != null
|
||||
? Text(_definition!)
|
||||
: const Text("defintion is null");
|
||||
case ActivityTypeEnum.lemmaId:
|
||||
return _lemma != null ? Text(_lemma!) : const Text("lemma is null");
|
||||
case ActivityTypeEnum.emoji:
|
||||
return _emoji != null ? Text(_emoji!) : const Text("emoji is null");
|
||||
default:
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: IntrinsicWidth(
|
||||
child: ConstrainedBox(
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: AppConfig.toolbarMinHeight),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints:
|
||||
const BoxConstraints(minWidth: AppConfig.toolbarMinWidth),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
EmojiPracticeButton(
|
||||
emoji: _emoji,
|
||||
token: widget.token,
|
||||
onPressed: () => _setActivityType(
|
||||
_activityType == ActivityTypeEnum.emoji
|
||||
? null
|
||||
: ActivityTypeEnum.emoji,
|
||||
),
|
||||
setEmoji: _setEmoji,
|
||||
),
|
||||
WordTextWithAudioButton(
|
||||
text: widget.token.text.content,
|
||||
ttsController: widget.tts,
|
||||
eventID: widget.messageEvent.eventId,
|
||||
),
|
||||
LemmaWidget(
|
||||
token: widget.token,
|
||||
onPressed: () => _setActivityType(
|
||||
_activityType == ActivityTypeEnum.lemmaId
|
||||
? null
|
||||
: ActivityTypeEnum.lemmaId,
|
||||
),
|
||||
lemma: _lemma,
|
||||
setLemma: _setLemma,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_activityType != null)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (_showActivityCard(_activityType))
|
||||
PracticeActivityCard(
|
||||
pangeaMessageEvent: widget.messageEvent,
|
||||
targetTokensAndActivityType:
|
||||
TargetTokensAndActivityType(
|
||||
tokens: [widget.token],
|
||||
activityType: _activityType!,
|
||||
),
|
||||
overlayController: widget.overlayController,
|
||||
morphFeature: _selectedMorphFeature,
|
||||
wordDetailsController: this,
|
||||
)
|
||||
else
|
||||
_activityAnswer,
|
||||
],
|
||||
)
|
||||
else
|
||||
ContextualTranslationWidget(
|
||||
token: widget.token,
|
||||
fullText: widget.messageEvent.messageDisplayText,
|
||||
langCode: widget.messageEvent.messageDisplayLangCode,
|
||||
onPressed: () =>
|
||||
_setActivityType(ActivityTypeEnum.wordMeaning),
|
||||
definition: _definition,
|
||||
setDefinition: _setDefinition,
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
MorphologicalListWidget(
|
||||
token: widget.token,
|
||||
setMorphFeature: _setSelectedMorphFeature,
|
||||
selectedMorphFeature: _selectedMorphFeature,
|
||||
completedActivities: completedMorphActivities,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -117,6 +117,7 @@ abstract class ClientManager {
|
|||
PangeaEventTypes.botOptions,
|
||||
PangeaEventTypes.capacity,
|
||||
EventTypes.RoomPowerLevels,
|
||||
PangeaEventTypes.userChosenEmoji,
|
||||
// Pangea#
|
||||
},
|
||||
logLevel: kReleaseMode ? Level.warning : Level.verbose,
|
||||
|
|
|
|||
|
|
@ -353,6 +353,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
dart_levenshtein:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dart_levenshtein
|
||||
sha256: f38182278b774cbb0d5993de50848e65da5a9f722b7f07787f0c26de009e0322
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
dart_webrtc:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ dependencies:
|
|||
rive: 0.11.11
|
||||
text_to_speech: ^0.2.3
|
||||
flutter_tts: ^4.2.0
|
||||
dart_levenshtein: ^1.0.1
|
||||
# Pangea#
|
||||
|
||||
dev_dependencies:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue