resolve merge conflicts
This commit is contained in:
commit
9d49a5542d
26 changed files with 984 additions and 484 deletions
|
|
@ -23,7 +23,7 @@ void main() async {
|
|||
|
||||
// #Pangea
|
||||
try {
|
||||
await dotenv.load(fileName: ".env");
|
||||
await dotenv.load(fileName: ".env.local_choreo");
|
||||
} catch (e) {
|
||||
Logs().e('Failed to load .env file', e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import 'package:fluffychat/pages/chat/chat.dart';
|
|||
import 'package:fluffychat/pages/chat/events/video_player.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/chat/message_token_text_stateful.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar_selection_area.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_message_text.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/pangea_rich_text.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
|
|
@ -306,13 +306,23 @@ class MessageContent extends StatelessWidget {
|
|||
height: 1.3,
|
||||
);
|
||||
|
||||
if (overlayController != null && pangeaMessageEvent != null) {
|
||||
return OverlayMessageText(
|
||||
pangeaMessageEvent: pangeaMessageEvent!,
|
||||
overlayController: overlayController!,
|
||||
if (pangeaMessageEvent?.messageDisplayRepresentation?.tokens !=
|
||||
null) {
|
||||
return MessageTokenTextStateful(
|
||||
messageAnalyticsEntry:
|
||||
controller.pangeaController.getAnalytics.perMessage.get(
|
||||
pangeaMessageEvent!,
|
||||
false,
|
||||
)!,
|
||||
style: messageTextStyle,
|
||||
onClick: (token) => controller.showToolbar(pangeaMessageEvent!),
|
||||
);
|
||||
}
|
||||
|
||||
if (overlayController != null && pangeaMessageEvent != null) {
|
||||
return overlayController!.messageTokenText;
|
||||
}
|
||||
|
||||
if (immersionMode && pangeaMessageEvent != null) {
|
||||
return Flexible(
|
||||
child: PangeaRichText(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'dart:math';
|
|||
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
|
|
@ -19,16 +20,21 @@ import 'package:sentry_flutter/sentry_flutter.dart';
|
|||
/// A minimized version of AnalyticsController that get the logged in user's analytics
|
||||
class GetAnalyticsController {
|
||||
late PangeaController _pangeaController;
|
||||
late MessageAnalyticsController perMessage;
|
||||
final List<AnalyticsCacheEntry> _cache = [];
|
||||
StreamSubscription<AnalyticsUpdate>? _analyticsUpdateSubscription;
|
||||
StreamController<AnalyticsStreamUpdate> analyticsStream =
|
||||
StreamController.broadcast();
|
||||
|
||||
ConstructListModel constructListModel = ConstructListModel(uses: []);
|
||||
Completer<void>? initCompleter;
|
||||
Completer<void> initCompleter = Completer<void>();
|
||||
|
||||
GetAnalyticsController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
|
||||
perMessage = MessageAnalyticsController(
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
String? get _l2Code => _pangeaController.languageController.userL2?.langCode;
|
||||
|
|
@ -58,22 +64,25 @@ class GetAnalyticsController {
|
|||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (initCompleter != null) return;
|
||||
initCompleter = Completer<void>();
|
||||
if (initCompleter.isCompleted) return;
|
||||
|
||||
_analyticsUpdateSubscription ??= _pangeaController
|
||||
.putAnalytics.analyticsUpdateStream.stream
|
||||
.listen(_onAnalyticsUpdate);
|
||||
try {
|
||||
_analyticsUpdateSubscription ??= _pangeaController
|
||||
.putAnalytics.analyticsUpdateStream.stream
|
||||
.listen(_onAnalyticsUpdate);
|
||||
|
||||
await _pangeaController.putAnalytics.lastUpdatedCompleter.future;
|
||||
await _getConstructs();
|
||||
constructListModel.updateConstructs([
|
||||
...(_getConstructsLocal() ?? []),
|
||||
..._locallyCachedConstructs,
|
||||
]);
|
||||
_updateAnalyticsStream();
|
||||
|
||||
initCompleter!.complete();
|
||||
await _pangeaController.putAnalytics.lastUpdatedCompleter.future;
|
||||
await _getConstructs();
|
||||
constructListModel.updateConstructs([
|
||||
...(_getConstructsLocal() ?? []),
|
||||
..._locallyCachedConstructs,
|
||||
]);
|
||||
_updateAnalyticsStream();
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(e: err, s: s);
|
||||
} finally {
|
||||
if (!initCompleter.isCompleted) initCompleter.complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all cached analytics data.
|
||||
|
|
@ -81,8 +90,9 @@ class GetAnalyticsController {
|
|||
constructListModel.dispose();
|
||||
_analyticsUpdateSubscription?.cancel();
|
||||
_analyticsUpdateSubscription = null;
|
||||
initCompleter = null;
|
||||
initCompleter = Completer<void>();
|
||||
_cache.clear();
|
||||
// perMessage.dispose();
|
||||
}
|
||||
|
||||
Future<void> _onAnalyticsUpdate(AnalyticsUpdate analyticsUpdate) async {
|
||||
|
|
|
|||
169
lib/pangea/controllers/message_analytics_controller.dart
Normal file
169
lib/pangea/controllers/message_analytics_controller.dart
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/get_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/practice_activities.dart/message_activity_request.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Picks which tokens to do activities on and what types of activities to do
|
||||
/// Caches result so that we don't have to recompute it
|
||||
/// Most importantly, we can't do this in the state of a message widget because the state is disposed of and recreated
|
||||
/// If we decided that the first token should have a hidden word listening, we need to remember that
|
||||
/// Otherwise, the user might leave the chat, return, and see a different word hidden
|
||||
class MessageAnalyticsEntry {
|
||||
final DateTime createdAt = DateTime.now();
|
||||
|
||||
late List<TokenWithXP> tokensWithXp;
|
||||
|
||||
final PangeaMessageEvent pmEvent;
|
||||
|
||||
//
|
||||
bool isFirstTimeComputing = true;
|
||||
|
||||
TokenWithXP? nextActivityToken;
|
||||
ActivityTypeEnum? nextActivityType;
|
||||
|
||||
MessageAnalyticsEntry(this.pmEvent) {
|
||||
debugPrint('making MessageAnalyticsEntry: ${pmEvent.messageDisplayText}');
|
||||
if (pmEvent.messageDisplayRepresentation?.tokens == null) {
|
||||
throw Exception('No tokens in message in MessageAnalyticsEntry');
|
||||
}
|
||||
tokensWithXp = pmEvent.messageDisplayRepresentation!.tokens!
|
||||
.map((token) => TokenWithXP(token: token))
|
||||
.toList();
|
||||
|
||||
computeTargetTypesForMessage();
|
||||
}
|
||||
|
||||
List<TokenWithXP> get tokensThatCanBeHeard =>
|
||||
tokensWithXp.where((t) => t.token.canBeHeard).toList();
|
||||
|
||||
// compute target tokens within async wrapper that adds a 250ms delay
|
||||
// to avoid blocking the UI thread
|
||||
Future<void> computeTargetTypesForMessageAsync() async {
|
||||
await Future.delayed(const Duration(milliseconds: 250));
|
||||
computeTargetTypesForMessage();
|
||||
}
|
||||
|
||||
void computeTargetTypesForMessage() {
|
||||
// reset
|
||||
nextActivityToken = null;
|
||||
nextActivityType = null;
|
||||
|
||||
// compute target types for each token
|
||||
for (final token in tokensWithXp) {
|
||||
token.targetTypes = [];
|
||||
|
||||
if (!token.token.lemma.saveVocab) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token.daysSinceLastUse < 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token.eligibleForActivity(ActivityTypeEnum.wordMeaning) &&
|
||||
!token.didActivity(ActivityTypeEnum.wordMeaning)) {
|
||||
token.targetTypes.add(ActivityTypeEnum.wordMeaning);
|
||||
}
|
||||
|
||||
if (token.eligibleForActivity(ActivityTypeEnum.wordFocusListening) &&
|
||||
!token.didActivity(ActivityTypeEnum.wordFocusListening) &&
|
||||
tokensThatCanBeHeard.length > 3) {
|
||||
token.targetTypes.add(ActivityTypeEnum.wordFocusListening);
|
||||
}
|
||||
|
||||
if (token.eligibleForActivity(ActivityTypeEnum.hiddenWordListening) &&
|
||||
isFirstTimeComputing &&
|
||||
!token.didActivity(ActivityTypeEnum.hiddenWordListening) &&
|
||||
!pmEvent.ownMessage) {
|
||||
token.targetTypes.add(ActivityTypeEnum.hiddenWordListening);
|
||||
}
|
||||
}
|
||||
|
||||
// from the tokens with hiddenWordListening in targetTypes, pick one at random
|
||||
final List<int> withListening = tokensWithXp
|
||||
.asMap()
|
||||
.entries
|
||||
.where(
|
||||
(entry) => entry.value.targetTypes
|
||||
.contains(ActivityTypeEnum.hiddenWordListening),
|
||||
)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
// randomly pick one entry in the list
|
||||
if (withListening.isNotEmpty) {
|
||||
final int randomIndex =
|
||||
withListening[Random().nextInt(withListening.length)];
|
||||
|
||||
nextActivityToken = tokensWithXp[randomIndex];
|
||||
nextActivityType = ActivityTypeEnum.hiddenWordListening;
|
||||
|
||||
// remove from all other tokens
|
||||
for (int i = 0; i < tokensWithXp.length; i++) {
|
||||
if (i != randomIndex) {
|
||||
tokensWithXp[i]
|
||||
.targetTypes
|
||||
.remove(ActivityTypeEnum.hiddenWordListening);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we didn't find any hiddenWordListening,
|
||||
// pick the first token that has a target type
|
||||
nextActivityToken ??=
|
||||
tokensWithXp.where((t) => t.targetTypes.isNotEmpty).firstOrNull;
|
||||
nextActivityType ??= nextActivityToken?.targetTypes.firstOrNull;
|
||||
|
||||
isFirstTimeComputing = false;
|
||||
}
|
||||
|
||||
bool get shouldHideToken => tokensWithXp.any(
|
||||
(token) =>
|
||||
token.targetTypes.contains(ActivityTypeEnum.hiddenWordListening),
|
||||
);
|
||||
}
|
||||
|
||||
/// computes TokenWithXP for given a pangeaMessageEvent and caches the result, according to the full text of the message
|
||||
/// listens for analytics updates and updates the cache accordingly
|
||||
class MessageAnalyticsController {
|
||||
final GetAnalyticsController getAnalytics;
|
||||
final Map<String, MessageAnalyticsEntry> _cache = {};
|
||||
|
||||
MessageAnalyticsController(this.getAnalytics);
|
||||
|
||||
void dispose() {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
// if over 50, remove oldest 5 entries by createdAt
|
||||
void clean() {
|
||||
if (_cache.length > 50) {
|
||||
final sortedEntries = _cache.entries.toList()
|
||||
..sort((a, b) => a.value.createdAt.compareTo(b.value.createdAt));
|
||||
for (var i = 0; i < 5; i++) {
|
||||
_cache.remove(sortedEntries[i].key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageAnalyticsEntry? get(
|
||||
PangeaMessageEvent pmEvent,
|
||||
bool refresh,
|
||||
) {
|
||||
if (pmEvent.messageDisplayRepresentation?.tokens == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_cache.containsKey(pmEvent.messageDisplayText) && !refresh) {
|
||||
return _cache[pmEvent.messageDisplayText];
|
||||
}
|
||||
|
||||
_cache[pmEvent.messageDisplayText] = MessageAnalyticsEntry(pmEvent);
|
||||
|
||||
clean();
|
||||
|
||||
return _cache[pmEvent.messageDisplayText];
|
||||
}
|
||||
}
|
||||
|
|
@ -90,6 +90,13 @@ class PracticeGenerationController {
|
|||
|
||||
final response = MessageActivityResponse.fromJson(json);
|
||||
|
||||
// workaround for the server not returning the tgtConstructs
|
||||
// if (response.activity != null &&
|
||||
// response.activity!.tgtConstructs.isEmpty) {
|
||||
// response.activity?.tgtConstructs.addAll(
|
||||
// requestModel.clientTokenRequest.constructIDs,
|
||||
// );
|
||||
// }
|
||||
return response;
|
||||
} else {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -105,6 +112,8 @@ class PracticeGenerationController {
|
|||
) async {
|
||||
final int cacheKey = req.hashCode;
|
||||
|
||||
// debugger(when: kDebugMode);
|
||||
|
||||
if (_cache.containsKey(cacheKey)) {
|
||||
return _cache[cacheKey]!.practiceActivity;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ class PutAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
(token) => OneConstructUse(
|
||||
useType: useType,
|
||||
lemma: token.lemma.text,
|
||||
form: token.lemma.form,
|
||||
form: token.text.content,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
metadata: metadata,
|
||||
category: token.pos,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,61 @@
|
|||
enum ActivityTypeEnum { multipleChoice, wordFocusListening }
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
|
||||
enum ActivityTypeEnum { wordMeaning, wordFocusListening, hiddenWordListening }
|
||||
|
||||
extension ActivityTypeExtension on ActivityTypeEnum {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ActivityTypeEnum.multipleChoice:
|
||||
return 'multiple_choice';
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return 'word_meaning';
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return 'word_focus_listening';
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return 'hidden_word_listening';
|
||||
}
|
||||
}
|
||||
|
||||
ActivityTypeEnum fromString(String value) {
|
||||
final split = value.split('.').last;
|
||||
switch (split) {
|
||||
// used to be called multiple_choice, but we changed it to word_meaning
|
||||
// as we now have multiple types of multiple choice activities
|
||||
// old data will still have multiple_choice so we need to handle that
|
||||
case 'multiple_choice':
|
||||
case 'multipleChoice':
|
||||
case 'word_meaning':
|
||||
case 'wordMeaning':
|
||||
return ActivityTypeEnum.wordMeaning;
|
||||
case 'word_focus_listening':
|
||||
case 'wordFocusListening':
|
||||
return ActivityTypeEnum.wordFocusListening;
|
||||
case 'hidden_word_listening':
|
||||
case 'hiddenWordListening':
|
||||
return ActivityTypeEnum.hiddenWordListening;
|
||||
default:
|
||||
throw Exception('Unknown activity type: $split');
|
||||
}
|
||||
}
|
||||
|
||||
List<ConstructUseTypeEnum> get associatedUseTypes {
|
||||
switch (this) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return [
|
||||
ConstructUseTypeEnum.corPA,
|
||||
ConstructUseTypeEnum.incPA,
|
||||
ConstructUseTypeEnum.ignPA
|
||||
];
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return [
|
||||
ConstructUseTypeEnum.corWL,
|
||||
ConstructUseTypeEnum.incWL,
|
||||
ConstructUseTypeEnum.ignWL
|
||||
];
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return [
|
||||
ConstructUseTypeEnum.corHWL,
|
||||
ConstructUseTypeEnum.incHWL,
|
||||
ConstructUseTypeEnum.ignHWL
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,12 +42,21 @@ enum ConstructUseTypeEnum {
|
|||
/// was target lemma in word-focus listening activity and correctly selected
|
||||
corWL,
|
||||
|
||||
/// form of lemma was read-aloud in word-focus listening activity and incorrectly selected
|
||||
/// a form of lemma was read-aloud in word-focus listening activity and incorrectly selected
|
||||
incWL,
|
||||
|
||||
/// form of lemma was read-aloud in word-focus listening activity and correctly ignored
|
||||
/// 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
|
||||
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,
|
||||
|
||||
/// not defined, likely a new construct introduced by choreo and not yet classified by an old version of the client
|
||||
nan
|
||||
}
|
||||
|
|
@ -71,12 +80,15 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.ignPA:
|
||||
case ConstructUseTypeEnum.ignWL:
|
||||
case ConstructUseTypeEnum.incWL:
|
||||
case ConstructUseTypeEnum.incHWL:
|
||||
case ConstructUseTypeEnum.ignHWL:
|
||||
return Icons.close;
|
||||
|
||||
case ConstructUseTypeEnum.ga:
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
case ConstructUseTypeEnum.corWL:
|
||||
case ConstructUseTypeEnum.corHWL:
|
||||
return Icons.check;
|
||||
|
||||
case ConstructUseTypeEnum.unk:
|
||||
|
|
@ -98,6 +110,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
|
||||
case ConstructUseTypeEnum.wa:
|
||||
case ConstructUseTypeEnum.corWL:
|
||||
case ConstructUseTypeEnum.corHWL:
|
||||
return 3;
|
||||
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
|
|
@ -110,6 +123,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.ignIGC:
|
||||
case ConstructUseTypeEnum.ignPA:
|
||||
case ConstructUseTypeEnum.ignWL:
|
||||
case ConstructUseTypeEnum.ignHWL:
|
||||
case ConstructUseTypeEnum.unk:
|
||||
case ConstructUseTypeEnum.nan:
|
||||
return 0;
|
||||
|
|
@ -123,6 +137,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
case ConstructUseTypeEnum.incWL:
|
||||
case ConstructUseTypeEnum.incHWL:
|
||||
return -3;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_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/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
|
|
@ -65,6 +65,7 @@ class ConstructListModel {
|
|||
category: use.category,
|
||||
);
|
||||
currentUses.uses.add(use);
|
||||
currentUses.setLastUsed(use.timeStamp);
|
||||
_constructMap[use.identifier.string] = currentUses;
|
||||
}
|
||||
}
|
||||
|
|
@ -149,38 +150,3 @@ class ConstructListModel {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// One lemma and a list of construct uses for that lemma
|
||||
class ConstructUses {
|
||||
final List<OneConstructUse> uses;
|
||||
final ConstructTypeEnum constructType;
|
||||
final String lemma;
|
||||
final String? _category;
|
||||
|
||||
ConstructUses({
|
||||
required this.uses,
|
||||
required this.constructType,
|
||||
required this.lemma,
|
||||
required category,
|
||||
}) : _category = category;
|
||||
|
||||
// Total points for all uses of this lemma
|
||||
int get points {
|
||||
return uses.fold<int>(
|
||||
0,
|
||||
(total, use) => total + use.useType.pointValue,
|
||||
);
|
||||
}
|
||||
|
||||
DateTime? _lastUsed;
|
||||
DateTime? get lastUsed {
|
||||
if (_lastUsed != null) return _lastUsed;
|
||||
final lastUse = uses.fold<DateTime?>(null, (DateTime? last, use) {
|
||||
if (last == null) return use.timeStamp;
|
||||
return use.timeStamp.isAfter(last) ? use.timeStamp : last;
|
||||
});
|
||||
return _lastUsed = lastUse;
|
||||
}
|
||||
|
||||
String get category => _category ?? "Other";
|
||||
}
|
||||
|
|
|
|||
61
lib/pangea/models/analytics/construct_use_model.dart
Normal file
61
lib/pangea/models/analytics/construct_use_model.dart
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
|
||||
/// One lemma and a list of construct uses for that lemma
|
||||
class ConstructUses {
|
||||
final List<OneConstructUse> uses;
|
||||
final ConstructTypeEnum constructType;
|
||||
final String lemma;
|
||||
final String? _category;
|
||||
DateTime? _lastUsed;
|
||||
|
||||
ConstructUses({
|
||||
required this.uses,
|
||||
required this.constructType,
|
||||
required this.lemma,
|
||||
required category,
|
||||
}) : _category = category;
|
||||
|
||||
// Total points for all uses of this lemma
|
||||
int get points {
|
||||
return uses.fold<int>(
|
||||
0,
|
||||
(total, use) => total + use.useType.pointValue,
|
||||
);
|
||||
}
|
||||
|
||||
DateTime? get lastUsed {
|
||||
if (_lastUsed != null) return _lastUsed;
|
||||
final lastUse = uses.fold<DateTime?>(null, (DateTime? last, use) {
|
||||
if (last == null) return use.timeStamp;
|
||||
return use.timeStamp.isAfter(last) ? use.timeStamp : last;
|
||||
});
|
||||
return _lastUsed = lastUse;
|
||||
}
|
||||
|
||||
void setLastUsed(DateTime time) {
|
||||
_lastUsed = time;
|
||||
}
|
||||
|
||||
String get category => _category ?? "Other";
|
||||
|
||||
ConstructIdentifier get id => ConstructIdentifier(
|
||||
lemma: lemma,
|
||||
type: constructType,
|
||||
category: category,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = {
|
||||
'construct_id': id.toJson(),
|
||||
'xp': points,
|
||||
'last_used': lastUsed?.toIso8601String(),
|
||||
|
||||
/// NOTE - sent to server as just the useTypes
|
||||
'uses': uses.map((e) => e.useType.string).toList(),
|
||||
};
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
|
@ -145,7 +145,8 @@ class OneConstructUse {
|
|||
for (final String category in morphCategoriesAndLabels.keys) {
|
||||
if (morphCategoriesAndLabels[category]!.contains(morphLemma)) {
|
||||
debugPrint(
|
||||
"found missing construct category for $morphLemma: $category");
|
||||
"found missing construct category for $morphLemma: $category",
|
||||
);
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import 'package:collection/collection.dart';
|
|||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.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:flutter/foundation.dart';
|
||||
|
||||
import '../constants/model_keys.dart';
|
||||
|
|
@ -13,6 +11,9 @@ import 'lemma.dart';
|
|||
|
||||
class PangeaToken {
|
||||
PangeaTokenText text;
|
||||
|
||||
//TODO - make this a string and move save_vocab to this class
|
||||
// clients have been able to handle null lemmas for 12 months so this is safe
|
||||
Lemma lemma;
|
||||
|
||||
/// [pos] ex "VERB" - part of speech of the token
|
||||
|
|
@ -120,37 +121,21 @@ class PangeaToken {
|
|||
/// alias for the end of the token ie offset + length
|
||||
int get end => text.offset + text.length;
|
||||
|
||||
/// create an empty tokenWithXP object
|
||||
TokenWithXP get emptyTokenWithXP {
|
||||
final List<ConstructWithXP> constructs = [];
|
||||
bool get isContentWord => ["NOUN", "VERB", "ADJ", "ADV"].contains(pos);
|
||||
|
||||
constructs.add(
|
||||
ConstructWithXP(
|
||||
id: ConstructIdentifier(
|
||||
lemma: lemma.text,
|
||||
type: ConstructTypeEnum.vocab,
|
||||
category: pos,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
for (final morph in morph.entries) {
|
||||
constructs.add(
|
||||
ConstructWithXP(
|
||||
id: ConstructIdentifier(
|
||||
lemma: morph.value,
|
||||
type: ConstructTypeEnum.morph,
|
||||
category: morph.key,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TokenWithXP(
|
||||
token: this,
|
||||
constructs: constructs,
|
||||
);
|
||||
}
|
||||
bool get canBeHeard => [
|
||||
"ADJ",
|
||||
"ADV",
|
||||
"AUX",
|
||||
"DET",
|
||||
"INTJ",
|
||||
"NOUN",
|
||||
"NUM",
|
||||
"PRON",
|
||||
"PROPN",
|
||||
"SCONJ",
|
||||
"VERB",
|
||||
].contains(pos);
|
||||
|
||||
/// Given a [type] and [metadata], returns a [OneConstructUse] for this lemma
|
||||
OneConstructUse toVocabUse(
|
||||
|
|
@ -160,7 +145,7 @@ class PangeaToken {
|
|||
return OneConstructUse(
|
||||
useType: type,
|
||||
lemma: lemma.text,
|
||||
form: lemma.form,
|
||||
form: text.content,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
metadata: metadata,
|
||||
category: pos,
|
||||
|
|
|
|||
|
|
@ -1,83 +1,59 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.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/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
|
||||
class ConstructWithXP {
|
||||
final ConstructIdentifier id;
|
||||
int xp;
|
||||
DateTime? lastUsed;
|
||||
List<ConstructUseTypeEnum> condensedConstructUses;
|
||||
|
||||
ConstructWithXP({
|
||||
required this.id,
|
||||
this.xp = 0,
|
||||
this.lastUsed,
|
||||
this.condensedConstructUses = const [],
|
||||
});
|
||||
|
||||
factory ConstructWithXP.fromJson(Map<String, dynamic> json) {
|
||||
return ConstructWithXP(
|
||||
id: ConstructIdentifier.fromJson(
|
||||
json['construct_id'] as Map<String, dynamic>,
|
||||
),
|
||||
xp: json['xp'] as int,
|
||||
lastUsed: json['last_used'] != null
|
||||
? DateTime.parse(json['last_used'] as String)
|
||||
: null,
|
||||
condensedConstructUses: (json['uses'] as List<String>).map((e) {
|
||||
return ConstructUseTypeUtil.fromString(e);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = {
|
||||
'construct_id': id.toJson(),
|
||||
'xp': xp,
|
||||
'last_used': lastUsed?.toIso8601String(),
|
||||
'uses': condensedConstructUses.map((e) => e.string).toList(),
|
||||
};
|
||||
return json;
|
||||
}
|
||||
}
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class TokenWithXP {
|
||||
final PangeaToken token;
|
||||
final List<ConstructWithXP> constructs;
|
||||
|
||||
DateTime? get lastUsed {
|
||||
return constructs.fold<DateTime?>(
|
||||
null,
|
||||
(previousValue, element) {
|
||||
if (previousValue == null) return element.lastUsed;
|
||||
if (element.lastUsed == null) return previousValue;
|
||||
return element.lastUsed!.isAfter(previousValue)
|
||||
? element.lastUsed
|
||||
: previousValue;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
int get xp {
|
||||
return constructs.fold<int>(
|
||||
0,
|
||||
(previousValue, element) => previousValue + element.xp,
|
||||
);
|
||||
}
|
||||
late List<ActivityTypeEnum> targetTypes;
|
||||
|
||||
TokenWithXP({
|
||||
required this.token,
|
||||
required this.constructs,
|
||||
});
|
||||
}) {
|
||||
targetTypes = [];
|
||||
}
|
||||
|
||||
List<ConstructIdentifier> get _constructIDs {
|
||||
final List<ConstructIdentifier> ids = [];
|
||||
ids.add(
|
||||
ConstructIdentifier(
|
||||
lemma: token.lemma.text,
|
||||
type: ConstructTypeEnum.vocab,
|
||||
category: token.pos,
|
||||
),
|
||||
);
|
||||
for (final morph in token.morph.entries) {
|
||||
ids.add(
|
||||
ConstructIdentifier(
|
||||
lemma: morph.value,
|
||||
type: ConstructTypeEnum.morph,
|
||||
category: morph.key,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
List<ConstructUses> get constructs => _constructIDs
|
||||
.map(
|
||||
(id) =>
|
||||
MatrixState.pangeaController.getAnalytics.constructListModel
|
||||
.getConstructUses(id) ??
|
||||
ConstructUses(
|
||||
lemma: id.lemma,
|
||||
constructType: id.type,
|
||||
category: id.category,
|
||||
uses: [],
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
factory TokenWithXP.fromJson(Map<String, dynamic> json) {
|
||||
return TokenWithXP(
|
||||
token: PangeaToken.fromJson(json['token'] as Map<String, dynamic>),
|
||||
constructs: (json['constructs'] as List)
|
||||
.map((e) => ConstructWithXP.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -85,21 +61,96 @@ class TokenWithXP {
|
|||
return {
|
||||
'token': token.toJson(),
|
||||
'constructs_with_xp': constructs.map((e) => e.toJson()).toList(),
|
||||
'target_types': targetTypes.map((e) => e.string).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
bool eligibleForActivity(ActivityTypeEnum a) {
|
||||
switch (a) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return token.isContentWord;
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return token.canBeHeard;
|
||||
}
|
||||
}
|
||||
|
||||
bool didActivity(ActivityTypeEnum a) {
|
||||
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() == token.text.content.toLowerCase(),)
|
||||
.map((u) => u.useType)
|
||||
.any((u) => a.associatedUseTypes.contains(u));
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return vocabConstruct.uses
|
||||
// TODO - double-check that form is going to be available here
|
||||
// .where((u) =>
|
||||
// u.form?.toLowerCase() == token.text.content.toLowerCase(),)
|
||||
.map((u) => u.useType)
|
||||
.any((u) => a.associatedUseTypes.contains(u));
|
||||
}
|
||||
}
|
||||
|
||||
ConstructUses get vocabConstruct {
|
||||
final vocab = constructs.firstWhereOrNull(
|
||||
(element) => element.id.type == ConstructTypeEnum.vocab,
|
||||
);
|
||||
if (vocab == null) {
|
||||
return ConstructUses(
|
||||
lemma: token.lemma.text,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
category: token.pos,
|
||||
uses: [],
|
||||
);
|
||||
}
|
||||
return vocab;
|
||||
}
|
||||
|
||||
int get xp {
|
||||
return constructs.fold<int>(
|
||||
0,
|
||||
(previousValue, element) => previousValue + element.points,
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
DateTime? get lastUsed => constructs.fold<DateTime?>(
|
||||
null,
|
||||
(previousValue, element) {
|
||||
if (previousValue == null) return element.lastUsed;
|
||||
if (element.lastUsed == null) return previousValue;
|
||||
return element.lastUsed!.isAfter(previousValue)
|
||||
? element.lastUsed
|
||||
: previousValue;
|
||||
},
|
||||
);
|
||||
|
||||
/// daysSinceLastUse
|
||||
int get daysSinceLastUse {
|
||||
if (lastUsed == null) return 1000;
|
||||
return DateTime.now().difference(lastUsed!).inDays;
|
||||
}
|
||||
|
||||
//override operator == and hashCode
|
||||
// check that the list of constructs are the same
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is TokenWithXP &&
|
||||
other.token.text == token.text &&
|
||||
other.lastUsed == lastUsed;
|
||||
const ListEquality().equals(other.constructs, constructs);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return token.text.hashCode ^ lastUsed.hashCode;
|
||||
return const ListEquality().hash(constructs);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -197,6 +248,10 @@ class MessageActivityRequest {
|
|||
|
||||
final List<ActivityTypeEnum> clientCompatibleActivities;
|
||||
|
||||
final ActivityTypeEnum clientTypeRequest;
|
||||
|
||||
final TokenWithXP clientTokenRequest;
|
||||
|
||||
MessageActivityRequest({
|
||||
required this.userL1,
|
||||
required this.userL2,
|
||||
|
|
@ -205,52 +260,10 @@ class MessageActivityRequest {
|
|||
required this.messageId,
|
||||
required this.existingActivities,
|
||||
required this.activityQualityFeedback,
|
||||
clientCompatibleActivities,
|
||||
}) : clientCompatibleActivities =
|
||||
clientCompatibleActivities ?? ActivityTypeEnum.values;
|
||||
|
||||
factory MessageActivityRequest.fromJson(Map<String, dynamic> json) {
|
||||
final clientCompatibleActivitiesEntry =
|
||||
json['client_version_compatible_activity_types'];
|
||||
List<ActivityTypeEnum>? clientCompatibleActivities;
|
||||
if (clientCompatibleActivitiesEntry != null &&
|
||||
clientCompatibleActivitiesEntry is List) {
|
||||
clientCompatibleActivities = clientCompatibleActivitiesEntry
|
||||
.map(
|
||||
(e) => ActivityTypeEnum.values.firstWhereOrNull(
|
||||
(element) =>
|
||||
element.string == e as String ||
|
||||
element.string.split('.').last == e,
|
||||
),
|
||||
)
|
||||
.where((entry) => entry != null)
|
||||
.cast<ActivityTypeEnum>()
|
||||
.toList();
|
||||
}
|
||||
return MessageActivityRequest(
|
||||
userL1: json['user_l1'] as String,
|
||||
userL2: json['user_l2'] as String,
|
||||
messageText: json['message_text'] as String,
|
||||
tokensWithXP: (json['tokens_with_xp'] as List)
|
||||
.map((e) => TokenWithXP.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
messageId: json['message_id'] as String,
|
||||
existingActivities: (json['existing_activities'] as List)
|
||||
.map(
|
||||
(e) => ExistingActivityMetaData.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
activityQualityFeedback: json['activity_quality_feedback'] != null
|
||||
? ActivityQualityFeedback.fromJson(
|
||||
json['activity_quality_feedback'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
clientCompatibleActivities: clientCompatibleActivities != null &&
|
||||
clientCompatibleActivities.isNotEmpty
|
||||
? clientCompatibleActivities
|
||||
: ActivityTypeEnum.values,
|
||||
);
|
||||
}
|
||||
required this.clientCompatibleActivities,
|
||||
required this.clientTokenRequest,
|
||||
required this.clientTypeRequest,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
|
|
@ -267,6 +280,8 @@ class MessageActivityRequest {
|
|||
// this for backwards compatibility with old clients
|
||||
'client_version_compatible_activity_types':
|
||||
clientCompatibleActivities.map((e) => e.string).toList(),
|
||||
'client_type_request': clientTypeRequest.string,
|
||||
'client_token_request': clientTokenRequest.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -224,13 +224,8 @@ class PracticeActivityModel {
|
|||
.toList(),
|
||||
langCode: json['lang_code'] as String,
|
||||
msgId: json['msg_id'] as String,
|
||||
activityType: json['activity_type'] == "multipleChoice"
|
||||
? ActivityTypeEnum.multipleChoice
|
||||
: ActivityTypeEnum.values.firstWhere(
|
||||
(e) =>
|
||||
e.string == json['activity_type'] as String ||
|
||||
e.string.split('.').last == json['activity_type'] as String,
|
||||
),
|
||||
activityType:
|
||||
ActivityTypeEnum.wordMeaning.fromString(json['activity_type']),
|
||||
content: ActivityContent.fromJson(contentMap),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
// finding the answer
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
|
|
@ -131,8 +132,22 @@ class ActivityRecordResponse {
|
|||
});
|
||||
|
||||
//TODO - differentiate into different activity types
|
||||
ConstructUseTypeEnum get useType =>
|
||||
score > 0 ? ConstructUseTypeEnum.corPA : ConstructUseTypeEnum.incPA;
|
||||
ConstructUseTypeEnum useType(ActivityTypeEnum aType) {
|
||||
switch (aType) {
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return score > 0
|
||||
? ConstructUseTypeEnum.corPA
|
||||
: ConstructUseTypeEnum.incPA;
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
return score > 0
|
||||
? ConstructUseTypeEnum.corWL
|
||||
: ConstructUseTypeEnum.incWL;
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
return score > 0
|
||||
? ConstructUseTypeEnum.corHWL
|
||||
: ConstructUseTypeEnum.incHWL;
|
||||
}
|
||||
}
|
||||
|
||||
// for each target construct create a OneConstructUse object
|
||||
List<OneConstructUse> toUses(
|
||||
|
|
@ -146,7 +161,7 @@ class ActivityRecordResponse {
|
|||
// TODO - add form to practiceActivity target_construct data somehow
|
||||
form: null,
|
||||
constructType: construct.type,
|
||||
useType: useType,
|
||||
useType: useType(practiceActivity.activityType),
|
||||
metadata: metadata,
|
||||
category: construct.category,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dar
|
|||
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:fluffychat/pangea/widgets/chat/message_token_text.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar_buttons.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_footer.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_header.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/target_tokens_controller.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -75,8 +75,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
bool isPlayingAudio = false;
|
||||
|
||||
bool get showToolbarButtons => !widget._pangeaMessageEvent.isAudioMessage;
|
||||
final TargetTokensController targetTokensController =
|
||||
TargetTokensController();
|
||||
|
||||
List<PangeaToken>? tokens;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -87,6 +87,8 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
const Duration(milliseconds: AppConfig.overlayAnimationDuration),
|
||||
);
|
||||
|
||||
_getTokens();
|
||||
|
||||
activitiesLeftToComplete = activitiesLeftToComplete -
|
||||
widget._pangeaMessageEvent.numberOfActivitiesCompleted;
|
||||
|
||||
|
|
@ -114,6 +116,39 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
setInitialToolbarMode();
|
||||
}
|
||||
|
||||
MessageTokenText get messageTokenText => MessageTokenText(
|
||||
ownMessage: pangeaMessageEvent.ownMessage,
|
||||
fullText: pangeaMessageEvent.messageDisplayText,
|
||||
tokensWithDisplay: tokens
|
||||
?.map(
|
||||
(token) => TokenWithDisplayInstructions(
|
||||
token: token,
|
||||
highlight: isTokenSelected(token),
|
||||
//NOTE: we actually do want the controller to be aware of which
|
||||
// tokens are currently being involved in activities and adjust here
|
||||
hideContent: false,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onClick: onClickOverlayMessageToken,
|
||||
);
|
||||
|
||||
Future<void> _getTokens() async {
|
||||
tokens = pangeaMessageEvent.originalSent?.tokens;
|
||||
|
||||
if (pangeaMessageEvent.originalSent != null && tokens == null) {
|
||||
pangeaMessageEvent.originalSent!
|
||||
.tokensGlobal(
|
||||
pangeaMessageEvent.senderId,
|
||||
pangeaMessageEvent.originServerTs,
|
||||
)
|
||||
.then((tokens) {
|
||||
// this isn't currently working because originalSent's _event is null
|
||||
setState(() => this.tokens = tokens);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// We need to check if the setState call is safe to call immediately
|
||||
/// Kept getting the error: setState() or markNeedsBuild() called during build.
|
||||
/// This is a workaround to prevent that error
|
||||
|
|
@ -493,7 +528,6 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
pangeaMessageEvent: widget._pangeaMessageEvent,
|
||||
overLayController: this,
|
||||
ttsController: tts,
|
||||
targetTokensController: targetTokensController,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
|
|
|
|||
138
lib/pangea/widgets/chat/message_token_text.dart
Normal file
138
lib/pangea/widgets/chat/message_token_text.dart
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MessageTokenText extends StatelessWidget {
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
|
||||
final bool ownMessage;
|
||||
|
||||
/// this must match the tokens or we've got problems
|
||||
final String fullText;
|
||||
|
||||
/// this must match the fullText or we've got problems
|
||||
final List<TokenWithDisplayInstructions>? tokensWithDisplay;
|
||||
final void Function(PangeaToken)? onClick;
|
||||
|
||||
MessageTokenText({
|
||||
super.key,
|
||||
required this.ownMessage,
|
||||
required this.fullText,
|
||||
required this.tokensWithDisplay,
|
||||
required this.onClick,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = TextStyle(
|
||||
color: ownMessage
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1.3,
|
||||
fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor,
|
||||
);
|
||||
|
||||
if (tokensWithDisplay == null || tokensWithDisplay!.isEmpty) {
|
||||
return Text(
|
||||
fullText,
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert the entire message into a list of characters
|
||||
final Characters messageCharacters = fullText.characters;
|
||||
|
||||
// When building token positions, use grapheme cluster indices
|
||||
final List<TokenPosition> tokenPositions = [];
|
||||
int globalIndex = 0;
|
||||
|
||||
for (int i = 0; i < tokensWithDisplay!.length; i++) {
|
||||
final tokenWithDisplay = tokensWithDisplay![i];
|
||||
final start = tokenWithDisplay.token.start;
|
||||
final end = tokenWithDisplay.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: tokenWithDisplay,
|
||||
),
|
||||
);
|
||||
globalIndex = endIndex;
|
||||
}
|
||||
|
||||
//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) {
|
||||
return TextSpan(
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onClick != null
|
||||
? onClick!(tokenPosition.token!.token)
|
||||
: null,
|
||||
text: !tokenPosition.token!.hideContent ? substring : '_____',
|
||||
style: style.merge(
|
||||
TextStyle(
|
||||
backgroundColor: tokenPosition.token!.highlight
|
||||
? 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 TokenWithDisplayInstructions {
|
||||
final PangeaToken token;
|
||||
final bool highlight;
|
||||
final bool hideContent;
|
||||
|
||||
TokenWithDisplayInstructions({
|
||||
required this.token,
|
||||
required this.highlight,
|
||||
required this.hideContent,
|
||||
});
|
||||
}
|
||||
|
||||
class TokenPosition {
|
||||
final int start;
|
||||
final int end;
|
||||
final TokenWithDisplayInstructions? token;
|
||||
final int tokenIndex;
|
||||
|
||||
const TokenPosition({
|
||||
required this.start,
|
||||
required this.end,
|
||||
this.token,
|
||||
this.tokenIndex = -1,
|
||||
});
|
||||
}
|
||||
125
lib/pangea/widgets/chat/message_token_text_stateful.dart
Normal file
125
lib/pangea/widgets/chat/message_token_text_stateful.dart
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_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/widgets/matrix.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Question - does this need to be stateful or does this work?
|
||||
/// Need to test.
|
||||
class MessageTokenTextStateful extends StatelessWidget {
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
|
||||
final MessageAnalyticsEntry messageAnalyticsEntry;
|
||||
|
||||
final TextStyle style;
|
||||
|
||||
final void Function(PangeaToken)? onClick;
|
||||
|
||||
bool get ownMessage => messageAnalyticsEntry.pmEvent.ownMessage;
|
||||
|
||||
MessageTokenTextStateful({
|
||||
super.key,
|
||||
required this.messageAnalyticsEntry,
|
||||
required this.style,
|
||||
required this.onClick,
|
||||
});
|
||||
|
||||
PangeaMessageEvent get pangeaMessageEvent => messageAnalyticsEntry.pmEvent;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Convert the entire message into a list of characters
|
||||
final Characters messageCharacters =
|
||||
pangeaMessageEvent.messageDisplayText.characters;
|
||||
|
||||
// When building token positions, use grapheme cluster indices
|
||||
final List<TokenPosition> tokenPositions = [];
|
||||
int globalIndex = 0;
|
||||
|
||||
for (final token in messageAnalyticsEntry.tokensWithXp) {
|
||||
final start = token.token.start;
|
||||
final end = token.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,
|
||||
hideContent: false,
|
||||
highlight: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
tokenPositions.add(
|
||||
TokenPosition(
|
||||
start: startIndex,
|
||||
end: endIndex,
|
||||
token: token.token,
|
||||
hideContent:
|
||||
token.targetTypes.contains(ActivityTypeEnum.hiddenWordListening),
|
||||
highlight: false,
|
||||
),
|
||||
);
|
||||
globalIndex = endIndex;
|
||||
}
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
children: tokenPositions.map((tokenPosition) {
|
||||
final substring = messageCharacters
|
||||
.skip(tokenPosition.start)
|
||||
.take(tokenPosition.end - tokenPosition.start)
|
||||
.toString();
|
||||
|
||||
if (tokenPosition.token != null) {
|
||||
return TextSpan(
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => onClick != null && tokenPosition.token != null
|
||||
? onClick!(tokenPosition.token!)
|
||||
: null,
|
||||
text: !tokenPosition.hideContent ? substring : '_____',
|
||||
style: style.merge(
|
||||
TextStyle(
|
||||
backgroundColor: tokenPosition.highlight
|
||||
? 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 bool highlight;
|
||||
final bool hideContent;
|
||||
final PangeaToken? token;
|
||||
|
||||
const TokenPosition({
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.hideContent,
|
||||
required this.highlight,
|
||||
this.token,
|
||||
});
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ 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/practice_activity/target_tokens_controller.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -28,14 +27,12 @@ class MessageToolbar extends StatelessWidget {
|
|||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final MessageOverlayController overLayController;
|
||||
final TtsController ttsController;
|
||||
final TargetTokensController targetTokensController;
|
||||
|
||||
const MessageToolbar({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.overLayController,
|
||||
required this.ttsController,
|
||||
required this.targetTokensController,
|
||||
});
|
||||
|
||||
Widget toolbarContent(BuildContext context) {
|
||||
|
|
@ -73,7 +70,9 @@ class MessageToolbar extends StatelessWidget {
|
|||
case MessageMode.definition:
|
||||
if (!overLayController.isSelection) {
|
||||
return FutureBuilder(
|
||||
future: targetTokensController.targetTokens(pangeaMessageEvent),
|
||||
//TODO - convert this to synchronous if possible
|
||||
future: Future.value(
|
||||
pangeaMessageEvent.messageDisplayRepresentation?.tokens),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.done) {
|
||||
return const ToolbarContentLoadingIndicator();
|
||||
|
|
@ -131,7 +130,6 @@ class MessageToolbar extends StatelessWidget {
|
|||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
overlayController: overLayController,
|
||||
ttsController: ttsController,
|
||||
targetTokensController: targetTokensController,
|
||||
);
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:fluffychat/widgets/matrix.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
// @ggurdin be great to explain the need/function of a widget like this
|
||||
class OverlayMessage extends StatelessWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:collection/collection.dart';
|
|||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/analytics_popup/analytics_xp_tile.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/grammar/get_grammar_copy.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
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/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class GeneratePracticeActivityButton extends StatelessWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final Function(PracticeActivityEvent?) onActivityGenerated;
|
||||
|
||||
const GeneratePracticeActivityButton({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.onActivityGenerated,
|
||||
});
|
||||
|
||||
//TODO - probably disable the generation of activities for specific messages
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () async {
|
||||
final String? l2Code = MatrixState.pangeaController.languageController
|
||||
.activeL1Model()
|
||||
?.langCode;
|
||||
|
||||
if (l2Code == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.noLanguagesSet),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw UnimplementedError();
|
||||
|
||||
// final PracticeActivityEvent? practiceActivityEvent = await MatrixState
|
||||
// .pangeaController.practiceGenerationController
|
||||
// .getPracticeActivity(
|
||||
// MessageActivityRequest(
|
||||
// candidateMessages: [
|
||||
// CandidateMessage(
|
||||
// msgId: pangeaMessageEvent.eventId,
|
||||
// roomId: pangeaMessageEvent.room.id,
|
||||
// text:
|
||||
// pangeaMessageEvent.representationByLanguage(l2Code)?.text ??
|
||||
// pangeaMessageEvent.body,
|
||||
// ),
|
||||
// ],
|
||||
// userIds: pangeaMessageEvent.room.client.userID != null
|
||||
// ? [pangeaMessageEvent.room.client.userID!]
|
||||
// : null,
|
||||
// ),
|
||||
// pangeaMessageEvent,
|
||||
// );
|
||||
|
||||
// onActivityGenerated(practiceActivityEvent);
|
||||
},
|
||||
child: Text(L10n.of(context)!.practice),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activ
|
|||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_style.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/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';
|
||||
|
|
@ -99,7 +100,10 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
|
||||
// If the selected choice is correct, send the record and get the next activity
|
||||
if (widget.currentActivity.content.isCorrect(value, index)) {
|
||||
widget.practiceCardController.onActivityFinish();
|
||||
MatrixState.pangeaController.getAnalytics.analyticsStream.stream.first
|
||||
.then((_) {
|
||||
widget.practiceCardController.onActivityFinish();
|
||||
});
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
|
|
@ -129,6 +133,17 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
ttsController: widget.tts,
|
||||
eventID: widget.eventID,
|
||||
),
|
||||
if (practiceActivity.activityType ==
|
||||
ActivityTypeEnum.hiddenWordListening)
|
||||
MessageAudioCard(
|
||||
messageEvent:
|
||||
widget.practiceCardController.widget.pangeaMessageEvent,
|
||||
overlayController:
|
||||
widget.practiceCardController.widget.overlayController,
|
||||
tts: widget.practiceCardController.widget.overlayController.tts,
|
||||
setIsPlayingAudio: widget.practiceCardController.widget
|
||||
.overlayController.setIsPlayingAudio,
|
||||
),
|
||||
ChoicesArray(
|
||||
isLoading: false,
|
||||
uniqueKeyForLayerLink: (index) => "multiple_choice_$index",
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ 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/practice_activity/target_tokens_controller.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -32,14 +31,12 @@ class PracticeActivityCard extends StatefulWidget {
|
|||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
final TtsController ttsController;
|
||||
final TargetTokensController targetTokensController;
|
||||
|
||||
const PracticeActivityCard({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.overlayController,
|
||||
required this.ttsController,
|
||||
required this.targetTokensController,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -56,6 +53,30 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
List<PracticeActivityEvent> get practiceActivities =>
|
||||
widget.pangeaMessageEvent.practiceActivities;
|
||||
|
||||
PracticeActivityEvent? get existingActivityMatchingNeeds {
|
||||
final messageAnalyticsEntry = pangeaController.getAnalytics.perMessage
|
||||
.get(widget.pangeaMessageEvent, false);
|
||||
|
||||
if (messageAnalyticsEntry?.nextActivityToken == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return null;
|
||||
}
|
||||
|
||||
for (final existingActivity in practiceActivities) {
|
||||
for (final c in messageAnalyticsEntry!.nextActivityToken!.constructs) {
|
||||
if (existingActivity.practiceActivity.tgtConstructs
|
||||
.any((tc) => tc == c.id) &&
|
||||
existingActivity.practiceActivity.activityType ==
|
||||
messageAnalyticsEntry.nextActivityType) {
|
||||
debugPrint('found existing activity');
|
||||
return existingActivity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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'
|
||||
|
|
@ -96,86 +117,118 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
/// If not, get a new activity from the server.
|
||||
Future<void> initialize() async {
|
||||
_setPracticeActivity(
|
||||
await _fetchNewActivity(),
|
||||
await _fetchActivity(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<PracticeActivityModel?> _fetchNewActivity([
|
||||
Future<PracticeActivityModel?> _fetchActivity([
|
||||
ActivityQualityFeedback? activityFeedback,
|
||||
]) async {
|
||||
try {
|
||||
debugPrint('Fetching new activity');
|
||||
// temporary
|
||||
// try {
|
||||
debugPrint('Fetching activity');
|
||||
// debugger();
|
||||
_updateFetchingActivity(true);
|
||||
|
||||
_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 (!pangeaController.languageController.languagesSet) {
|
||||
debugger(when: kDebugMode);
|
||||
_updateFetchingActivity(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
debugger(when: kDebugMode);
|
||||
_updateFetchingActivity(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (widget.pangeaMessageEvent.messageDisplayRepresentation == null) {
|
||||
debugger(when: kDebugMode);
|
||||
_updateFetchingActivity(false);
|
||||
ErrorHandler.logError(
|
||||
e: Exception('No original message found in _fetchNewActivity'),
|
||||
data: {
|
||||
'event': widget.pangeaMessageEvent.event.toJson(),
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final PracticeActivityModelResponse? activityResponse =
|
||||
await pangeaController.practiceGenerationController
|
||||
.getPracticeActivity(
|
||||
MessageActivityRequest(
|
||||
userL1: pangeaController.languageController.userL1!.langCode,
|
||||
userL2: pangeaController.languageController.userL2!.langCode,
|
||||
messageText: widget.pangeaMessageEvent.messageDisplayText,
|
||||
tokensWithXP: await widget.targetTokensController.targetTokens(
|
||||
widget.pangeaMessageEvent,
|
||||
),
|
||||
messageId: widget.pangeaMessageEvent.eventId,
|
||||
existingActivities: practiceActivities
|
||||
.map((activity) => activity.activityRequestMetaData)
|
||||
.toList(),
|
||||
activityQualityFeedback: activityFeedback,
|
||||
clientCompatibleActivities: widget
|
||||
.ttsController.isLanguageFullySupported
|
||||
? ActivityTypeEnum.values
|
||||
: ActivityTypeEnum.values
|
||||
.where((type) => type != ActivityTypeEnum.wordFocusListening)
|
||||
.toList(),
|
||||
),
|
||||
widget.pangeaMessageEvent,
|
||||
);
|
||||
|
||||
currentActivityCompleter = activityResponse?.eventCompleter;
|
||||
_updateFetchingActivity(false);
|
||||
|
||||
return activityResponse?.activity;
|
||||
} catch (e, s) {
|
||||
// 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 (!pangeaController.languageController.languagesSet) {
|
||||
debugger(when: kDebugMode);
|
||||
_updateFetchingActivity(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
debugger(when: kDebugMode);
|
||||
_updateFetchingActivity(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (widget.pangeaMessageEvent.messageDisplayRepresentation == null) {
|
||||
debugger(when: kDebugMode);
|
||||
_updateFetchingActivity(false);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
m: 'Failed to get new activity',
|
||||
e: Exception('No original message found in _fetchNewActivity'),
|
||||
data: {
|
||||
'activity': currentActivity,
|
||||
'record': currentCompletionRecord,
|
||||
'event': widget.pangeaMessageEvent.event.toJson(),
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (widget.pangeaMessageEvent.messageDisplayRepresentation?.tokens ==
|
||||
null) {
|
||||
debugger(when: kDebugMode);
|
||||
_updateFetchingActivity(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
final messageAnalyticsEntry = pangeaController.getAnalytics.perMessage
|
||||
.get(widget.pangeaMessageEvent, false);
|
||||
|
||||
// the client is going to be choosing the next activity now
|
||||
// if nothing is set then it must be done with practice
|
||||
if (messageAnalyticsEntry?.nextActivityToken == null ||
|
||||
messageAnalyticsEntry?.nextActivityType == null) {
|
||||
debugger(when: kDebugMode);
|
||||
_updateFetchingActivity(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
final existingActivity = existingActivityMatchingNeeds;
|
||||
|
||||
if (existingActivity != null) {
|
||||
return existingActivity.practiceActivity;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
"client requesting activity type: ${messageAnalyticsEntry?.nextActivityType}",
|
||||
);
|
||||
debugPrint(
|
||||
"client requesting token: ${messageAnalyticsEntry?.nextActivityToken?.token.text.content}",
|
||||
);
|
||||
|
||||
final PracticeActivityModelResponse? activityResponse =
|
||||
await pangeaController.practiceGenerationController.getPracticeActivity(
|
||||
MessageActivityRequest(
|
||||
userL1: pangeaController.languageController.userL1!.langCode,
|
||||
userL2: pangeaController.languageController.userL2!.langCode,
|
||||
messageText: widget.pangeaMessageEvent.originalSent!.text,
|
||||
tokensWithXP: messageAnalyticsEntry!.tokensWithXp,
|
||||
messageId: widget.pangeaMessageEvent.eventId,
|
||||
existingActivities: practiceActivities
|
||||
.map((activity) => activity.activityRequestMetaData)
|
||||
.toList(),
|
||||
activityQualityFeedback: activityFeedback,
|
||||
clientCompatibleActivities: widget
|
||||
.ttsController.isLanguageFullySupported
|
||||
? ActivityTypeEnum.values
|
||||
: ActivityTypeEnum.values
|
||||
.where((type) => type != ActivityTypeEnum.wordFocusListening)
|
||||
.toList(),
|
||||
clientTokenRequest: messageAnalyticsEntry.nextActivityToken!,
|
||||
clientTypeRequest: messageAnalyticsEntry.nextActivityType!,
|
||||
),
|
||||
widget.pangeaMessageEvent,
|
||||
);
|
||||
|
||||
currentActivityCompleter = activityResponse?.eventCompleter;
|
||||
_updateFetchingActivity(false);
|
||||
|
||||
return activityResponse?.activity;
|
||||
// } catch (e, s) {
|
||||
// debugger(when: kDebugMode);
|
||||
// ErrorHandler.logError(
|
||||
// e: e,
|
||||
// s: s,
|
||||
// m: 'Failed to get new activity',
|
||||
// data: {
|
||||
// 'activity': currentActivity,
|
||||
// 'record': currentCompletionRecord,
|
||||
// },
|
||||
// );
|
||||
// return null;
|
||||
// }
|
||||
}
|
||||
|
||||
ConstructUseMetaData get metadata => ConstructUseMetaData(
|
||||
|
|
@ -220,13 +273,17 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
|
||||
// update the target tokens with the new construct uses
|
||||
// NOTE - multiple choice activity is handling adding these to analytics
|
||||
await widget.targetTokensController.updateTokensWithConstructs(
|
||||
currentCompletionRecord!.usesForAllResponses(
|
||||
currentActivity!,
|
||||
metadata,
|
||||
),
|
||||
widget.pangeaMessageEvent,
|
||||
);
|
||||
|
||||
// previously we would update the tokens with the constructs
|
||||
// now the tokens themselves calculate their own points using the analytics
|
||||
// we're going to see if this creates performance issues
|
||||
final messageAnalytics = MatrixState
|
||||
.pangeaController.getAnalytics.perMessage
|
||||
.get(widget.pangeaMessageEvent, false);
|
||||
// messageAnalytics will only be null if there are no tokens to update
|
||||
|
||||
// set the target types for the next activity
|
||||
messageAnalytics!.computeTargetTypesForMessageAsync();
|
||||
|
||||
widget.overlayController.onActivityFinish();
|
||||
pangeaController.activityRecordController.completeActivity(
|
||||
|
|
@ -237,7 +294,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
// and setting it to replace the previous activity
|
||||
final Iterable<dynamic> result = await Future.wait([
|
||||
_savorTheJoy(),
|
||||
_fetchNewActivity(),
|
||||
_fetchActivity(),
|
||||
]);
|
||||
|
||||
_setPracticeActivity(result.last as PracticeActivityModel?);
|
||||
|
|
@ -279,7 +336,7 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
);
|
||||
}
|
||||
|
||||
_fetchNewActivity(
|
||||
_fetchActivity(
|
||||
ActivityQualityFeedback(
|
||||
feedbackText: feedback,
|
||||
badActivity: currentActivity!,
|
||||
|
|
@ -315,34 +372,15 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
switch (currentActivity?.activityType) {
|
||||
case null:
|
||||
return null;
|
||||
case ActivityTypeEnum.multipleChoice:
|
||||
return MultipleChoiceActivity(
|
||||
practiceCardController: this,
|
||||
currentActivity: currentActivity!,
|
||||
tts: widget.ttsController,
|
||||
eventID: widget.pangeaMessageEvent.eventId,
|
||||
);
|
||||
case ActivityTypeEnum.wordFocusListening:
|
||||
// return WordFocusListeningActivity(
|
||||
// activity: currentActivity!, practiceCardController: this);
|
||||
case ActivityTypeEnum.hiddenWordListening:
|
||||
case ActivityTypeEnum.wordMeaning:
|
||||
return MultipleChoiceActivity(
|
||||
practiceCardController: this,
|
||||
currentActivity: currentActivity!,
|
||||
tts: widget.ttsController,
|
||||
eventID: widget.pangeaMessageEvent.eventId,
|
||||
);
|
||||
// default:
|
||||
// ErrorHandler.logError(
|
||||
// e: Exception('Unknown activity type'),
|
||||
// m: 'Unknown activity type',
|
||||
// data: {
|
||||
// 'activityType': currentActivity!.activityType,
|
||||
// },
|
||||
// );
|
||||
// return Text(
|
||||
// L10n.of(context)!.oopsSomethingWentWrong,
|
||||
// style: BotStyle.text(context),
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_list_model.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:flutter/foundation.dart';
|
||||
|
||||
/// Seperated out the target tokens from the practice activity card
|
||||
/// in order to control the state of the target tokens
|
||||
class TargetTokensController {
|
||||
List<TokenWithXP>? _targetTokens;
|
||||
|
||||
TargetTokensController();
|
||||
|
||||
/// From the tokens in the message, do a preliminary filtering of which to target
|
||||
/// Then get the construct uses for those tokens
|
||||
Future<List<TokenWithXP>> targetTokens(
|
||||
PangeaMessageEvent pangeaMessageEvent,
|
||||
) async {
|
||||
if (_targetTokens != null) {
|
||||
return _targetTokens!;
|
||||
}
|
||||
|
||||
_targetTokens = await _initialize(pangeaMessageEvent);
|
||||
|
||||
// final allConstructs = MatrixState
|
||||
// .pangeaController.getAnalytics.analyticsStream.value?.constructs;
|
||||
// await updateTokensWithConstructs(
|
||||
// allConstructs ?? [],
|
||||
// pangeaMessageEvent,
|
||||
// );
|
||||
|
||||
return _targetTokens!;
|
||||
}
|
||||
|
||||
Future<List<TokenWithXP>> _initialize(
|
||||
PangeaMessageEvent pangeaMessageEvent,
|
||||
) async {
|
||||
final tokens =
|
||||
await pangeaMessageEvent.messageDisplayRepresentation?.tokensGlobal(
|
||||
pangeaMessageEvent.senderId,
|
||||
pangeaMessageEvent.originServerTs,
|
||||
);
|
||||
|
||||
if (tokens == null || tokens.isEmpty) {
|
||||
debugger(when: kDebugMode);
|
||||
return _targetTokens = [];
|
||||
}
|
||||
|
||||
return _targetTokens =
|
||||
tokens.map((token) => token.emptyTokenWithXP).toList();
|
||||
}
|
||||
|
||||
Future<void> updateTokensWithConstructs(
|
||||
List<OneConstructUse> constructUses,
|
||||
pangeaMessageEvent,
|
||||
) async {
|
||||
final ConstructListModel constructList = ConstructListModel(
|
||||
uses: constructUses,
|
||||
// type: null,
|
||||
);
|
||||
|
||||
_targetTokens ??= await _initialize(pangeaMessageEvent);
|
||||
|
||||
for (final token in _targetTokens!) {
|
||||
// we don't need to do this for tokens that don't have saveVocab set to true
|
||||
if (!token.token.lemma.saveVocab) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final construct in token.constructs) {
|
||||
final constructUseModel = constructList.getConstructUses(
|
||||
ConstructIdentifier(
|
||||
lemma: construct.id.lemma,
|
||||
type: construct.id.type,
|
||||
category: construct.id.category,
|
||||
),
|
||||
);
|
||||
if (constructUseModel != null) {
|
||||
construct.xp += constructUseModel.points;
|
||||
construct.lastUsed = constructUseModel.lastUsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue