passing practice model instead of activity?
This commit is contained in:
parent
26752a9ba7
commit
8bffe17455
13 changed files with 433 additions and 334 deletions
|
|
@ -22,7 +22,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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
|||
import 'package:fluffychat/pangea/enum/message_mode_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/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
|
|
@ -652,15 +653,25 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
// There's a listen in my_analytics_controller that decides when to auto-update
|
||||
// analytics based on when / how many messages the logged in user send. This
|
||||
// stream sends the data for newly sent messages.
|
||||
final metadata = ConstructUseMetaData(
|
||||
roomId: roomId,
|
||||
timeStamp: DateTime.now(),
|
||||
eventId: msgEventId,
|
||||
);
|
||||
|
||||
if (msgEventId != null) {
|
||||
pangeaController.myAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
eventId: msgEventId,
|
||||
eventType: EventTypes.Message,
|
||||
roomId: room.id,
|
||||
originalSent: originalSent,
|
||||
tokensSent: tokensSent,
|
||||
choreo: choreo,
|
||||
constructs: [
|
||||
...(choreo!.grammarConstructUses(metadata: metadata)),
|
||||
...(originalSent!.vocabUses(
|
||||
choreo: choreo,
|
||||
tokens: tokensSent!.tokens,
|
||||
metadata: metadata,
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,25 +67,23 @@ class ChoicesArrayState extends State<ChoicesArray> {
|
|||
: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: widget.choices!
|
||||
.mapIndexed(
|
||||
(index, entry) => ChoiceItem(
|
||||
theme: theme,
|
||||
onLongPress:
|
||||
widget.isActive ? widget.onLongPress : null,
|
||||
onPressed: widget.isActive
|
||||
? widget.onPressed
|
||||
: (String value, int index) {
|
||||
debugger(when: kDebugMode);
|
||||
},
|
||||
entry: MapEntry(index, entry),
|
||||
interactionDisabled: interactionDisabled,
|
||||
enableInteraction: enableInteractions,
|
||||
disableInteraction: disableInteraction,
|
||||
isSelected: widget.selectedChoiceIndex == index,
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
[],
|
||||
.mapIndexed(
|
||||
(index, entry) => ChoiceItem(
|
||||
theme: theme,
|
||||
onLongPress: widget.isActive ? widget.onLongPress : null,
|
||||
onPressed: widget.isActive
|
||||
? widget.onPressed
|
||||
: (String value, int index) {
|
||||
debugger(when: kDebugMode);
|
||||
},
|
||||
entry: MapEntry(index, entry),
|
||||
interactionDisabled: interactionDisabled,
|
||||
enableInteraction: enableInteractions,
|
||||
disableInteraction: disableInteraction,
|
||||
isSelected: widget.selectedChoiceIndex == index,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/base_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.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/extensions/pangea_room_extension/pangea_room_extension.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/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -29,7 +23,7 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
late PangeaController _pangeaController;
|
||||
CachedStreamController<AnalyticsUpdateType> analyticsUpdateStream =
|
||||
CachedStreamController<AnalyticsUpdateType>();
|
||||
StreamSubscription<AnalyticsStream>? _messageSendSubscription;
|
||||
StreamSubscription<AnalyticsStream>? _analyticsStream;
|
||||
Timer? _updateTimer;
|
||||
|
||||
Client get _client => _pangeaController.matrixState.client;
|
||||
|
|
@ -60,7 +54,7 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
void initialize() {
|
||||
// Listen to a stream that provides the eventIDs
|
||||
// of new messages sent by the logged in user
|
||||
_messageSendSubscription ??=
|
||||
_analyticsStream ??=
|
||||
stateStream.listen((data) => _onNewAnalyticsData(data));
|
||||
|
||||
_refreshAnalyticsIfOutdated();
|
||||
|
|
@ -72,8 +66,8 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
_updateTimer?.cancel();
|
||||
lastUpdated = null;
|
||||
lastUpdatedCompleter = Completer<DateTime?>();
|
||||
_messageSendSubscription?.cancel();
|
||||
_messageSendSubscription = null;
|
||||
_analyticsStream?.cancel();
|
||||
_analyticsStream = null;
|
||||
_refreshAnalyticsIfOutdated();
|
||||
clearMessagesSinceUpdate();
|
||||
}
|
||||
|
|
@ -109,34 +103,9 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
/// Given the data from a newly sent message, format and cache
|
||||
/// the message's construct data locally and reset the update timer
|
||||
void _onNewAnalyticsData(AnalyticsStream data) {
|
||||
// convert that data into construct uses and add it to the cache
|
||||
final metadata = ConstructUseMetaData(
|
||||
roomId: data.roomId,
|
||||
eventId: data.eventId,
|
||||
timeStamp: DateTime.now(),
|
||||
);
|
||||
|
||||
final List<OneConstructUse> constructs = _getDraftUses(data.roomId);
|
||||
|
||||
if (data.eventType == EventTypes.Message) {
|
||||
constructs.addAll([
|
||||
...(data.choreo!.grammarConstructUses(metadata: metadata)),
|
||||
...(data.originalSent!.vocabUses(
|
||||
choreo: data.choreo,
|
||||
tokens: data.tokensSent!.tokens,
|
||||
metadata: metadata,
|
||||
)),
|
||||
]);
|
||||
} else if (data.eventType == PangeaEventTypes.activityRecord &&
|
||||
data.practiceActivity != null) {
|
||||
final activityConstructs = data.recordModel!.uses(
|
||||
data.practiceActivity!,
|
||||
metadata: metadata,
|
||||
);
|
||||
constructs.addAll(activityConstructs);
|
||||
} else {
|
||||
throw PangeaWarningError("Invalid event type for analytics stream");
|
||||
}
|
||||
constructs.addAll(data.constructs);
|
||||
|
||||
final String eventID = data.eventId;
|
||||
final String roomID = data.roomId;
|
||||
|
|
@ -342,43 +311,13 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
|||
|
||||
class AnalyticsStream {
|
||||
final String eventId;
|
||||
final String eventType;
|
||||
final String roomId;
|
||||
|
||||
/// if the event is a message, the original message sent
|
||||
final PangeaRepresentation? originalSent;
|
||||
|
||||
/// if the event is a message, the tokens sent
|
||||
final PangeaMessageTokens? tokensSent;
|
||||
|
||||
/// if the event is a message, the choreo record
|
||||
final ChoreoRecord? choreo;
|
||||
|
||||
/// if the event is a practice activity, the practice activity event
|
||||
final PracticeActivityEvent? practiceActivity;
|
||||
|
||||
/// if the event is a practice activity, the record model
|
||||
final PracticeActivityRecordModel? recordModel;
|
||||
final List<OneConstructUse> constructs;
|
||||
|
||||
AnalyticsStream({
|
||||
required this.eventId,
|
||||
required this.eventType,
|
||||
required this.roomId,
|
||||
this.originalSent,
|
||||
this.tokensSent,
|
||||
this.choreo,
|
||||
this.practiceActivity,
|
||||
this.recordModel,
|
||||
}) {
|
||||
assert(
|
||||
(originalSent != null && tokensSent != null && choreo != null) ||
|
||||
(practiceActivity != null && recordModel != null),
|
||||
"Either a message or a practice activity must be provided",
|
||||
);
|
||||
|
||||
assert(
|
||||
eventType == EventTypes.Message ||
|
||||
eventType == PangeaEventTypes.activityRecord,
|
||||
);
|
||||
}
|
||||
required this.constructs,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,10 +119,28 @@ class PracticeGenerationController {
|
|||
requestModel: req,
|
||||
);
|
||||
|
||||
// if the server points to an existing event, return that event
|
||||
if (res.existingActivityEventId != null) {
|
||||
debugPrint(
|
||||
'Existing activity event found: ${res.existingActivityEventId}',
|
||||
);
|
||||
final Event? existingEvent =
|
||||
await event.room.getEventById(res.existingActivityEventId!);
|
||||
if (existingEvent != null) {
|
||||
return PracticeActivityEvent(
|
||||
event: existingEvent,
|
||||
timeline: event.timeline,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (res.activity == null) {
|
||||
debugPrint('No activity generated');
|
||||
return null;
|
||||
}
|
||||
|
||||
debugPrint('Activity generated: ${res.activity!.toJson()}');
|
||||
|
||||
final Future<PracticeActivityEvent?> eventFuture =
|
||||
_sendAndPackageEvent(res.activity!, event);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:developer';
|
|||
|
||||
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.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 'package:matrix/matrix.dart';
|
||||
|
|
@ -62,24 +63,34 @@ class PracticeActivityEvent {
|
|||
|
||||
/// Completion record assosiated with this activity
|
||||
/// for the logged in user, null if there is none
|
||||
PracticeActivityRecordEvent? get userRecord {
|
||||
final List<PracticeActivityRecordEvent> records = allRecords
|
||||
.where(
|
||||
(recordEvent) =>
|
||||
recordEvent.event.senderId ==
|
||||
recordEvent.event.room.client.userID,
|
||||
)
|
||||
.toList();
|
||||
if (records.length > 1) {
|
||||
debugPrint("There should only be one record per user per activity");
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
return records.firstOrNull;
|
||||
List<PracticeActivityRecordEvent> get allUserRecords => allRecords
|
||||
.where(
|
||||
(recordEvent) =>
|
||||
recordEvent.event.senderId == recordEvent.event.room.client.userID,
|
||||
)
|
||||
.toList();
|
||||
|
||||
/// Get the most recent user record for this activity
|
||||
PracticeActivityRecordEvent? get latestUserRecord {
|
||||
final List<PracticeActivityRecordEvent> userRecords = allUserRecords;
|
||||
if (userRecords.isEmpty) return null;
|
||||
return userRecords.reduce(
|
||||
(a, b) => a.event.originServerTs.isAfter(b.event.originServerTs) ? a : b,
|
||||
);
|
||||
}
|
||||
|
||||
DateTime? get lastCompletedAt => latestUserRecord?.event.originServerTs;
|
||||
|
||||
String get parentMessageId => event.relationshipEventId!;
|
||||
|
||||
/// Checks if there are any user records in the list for this activity,
|
||||
/// and, if so, then the activity is complete
|
||||
bool get isComplete => userRecord != null;
|
||||
bool get isComplete => latestUserRecord != null;
|
||||
|
||||
ExistingActivityMetaData get activityRequestMetaData =>
|
||||
ExistingActivityMetaData(
|
||||
activityEventId: event.eventId,
|
||||
tgtConstructs: practiceActivity.tgtConstructs,
|
||||
activityType: practiceActivity.activityType,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,13 +34,11 @@ class PangeaToken {
|
|||
int endTokenIndex = -1,
|
||||
]) {
|
||||
if (endTokenIndex == -1) {
|
||||
endTokenIndex = tokens.length - 1;
|
||||
endTokenIndex = tokens.length;
|
||||
}
|
||||
|
||||
final List<PangeaToken> subset =
|
||||
tokens.whereIndexed((int index, PangeaToken token) {
|
||||
return index >= startTokenIndex && index <= endTokenIndex;
|
||||
}).toList();
|
||||
tokens.sublist(startTokenIndex, endTokenIndex);
|
||||
|
||||
if (subset.isEmpty) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -51,10 +49,11 @@ class PangeaToken {
|
|||
return subset.first.text.content;
|
||||
}
|
||||
|
||||
String reconstruction = subset.first.text.content;
|
||||
for (int i = 1; i < subset.length - 1; i++) {
|
||||
String reconstruction = "";
|
||||
for (int i = 0; i < subset.length; i++) {
|
||||
int whitespace = subset[i].text.offset -
|
||||
(subset[i - 1].text.offset + subset[i - 1].text.length);
|
||||
(i > 0 ? (subset[i - 1].text.offset + subset[i - 1].text.length) : 0);
|
||||
|
||||
if (whitespace < 0) {
|
||||
debugger(when: kDebugMode);
|
||||
whitespace = 0;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import 'package:collection/collection.dart';
|
||||
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/practice_activity_model.dart';
|
||||
|
||||
class ConstructWithXP {
|
||||
final ConstructIdentifier id;
|
||||
int xp;
|
||||
final DateTime? lastUsed;
|
||||
DateTime? lastUsed;
|
||||
|
||||
ConstructWithXP({
|
||||
required this.id,
|
||||
|
|
@ -94,13 +95,52 @@ class TokenWithXP {
|
|||
}
|
||||
}
|
||||
|
||||
class ExistingActivityMetaData {
|
||||
final String activityEventId;
|
||||
final List<ConstructIdentifier> tgtConstructs;
|
||||
final ActivityTypeEnum activityType;
|
||||
|
||||
ExistingActivityMetaData({
|
||||
required this.activityEventId,
|
||||
required this.tgtConstructs,
|
||||
required this.activityType,
|
||||
});
|
||||
|
||||
factory ExistingActivityMetaData.fromJson(Map<String, dynamic> json) {
|
||||
return ExistingActivityMetaData(
|
||||
activityEventId: json['activity_event_id'] as String,
|
||||
tgtConstructs: (json['tgt_constructs'] as List)
|
||||
.map((e) => ConstructIdentifier.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
activityType: ActivityTypeEnum.values.firstWhere(
|
||||
(element) =>
|
||||
element.string == json['activity_type'] as String ||
|
||||
element.string.split('.').last == json['activity_type'] as String,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'activity_event_id': activityEventId,
|
||||
'tgt_constructs': tgtConstructs.map((e) => e.toJson()).toList(),
|
||||
'activity_type': activityType.string,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MessageActivityRequest {
|
||||
final String userL1;
|
||||
final String userL2;
|
||||
|
||||
final String messageText;
|
||||
|
||||
/// tokens with their associated constructs and xp
|
||||
final List<TokenWithXP> tokensWithXP;
|
||||
|
||||
/// make the server aware of existing activities for potential reuse
|
||||
final List<ExistingActivityMetaData> existingActivities;
|
||||
|
||||
final String messageId;
|
||||
|
||||
MessageActivityRequest({
|
||||
|
|
@ -109,6 +149,7 @@ class MessageActivityRequest {
|
|||
required this.messageText,
|
||||
required this.tokensWithXP,
|
||||
required this.messageId,
|
||||
required this.existingActivities,
|
||||
});
|
||||
|
||||
factory MessageActivityRequest.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -120,6 +161,11 @@ class MessageActivityRequest {
|
|||
.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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -130,6 +176,7 @@ class MessageActivityRequest {
|
|||
'message_text': messageText,
|
||||
'tokens_with_xp': tokensWithXP.map((e) => e.toJson()).toList(),
|
||||
'message_id': messageId,
|
||||
'existing_activities': existingActivities.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -152,10 +199,12 @@ class MessageActivityRequest {
|
|||
class MessageActivityResponse {
|
||||
final PracticeActivityModel? activity;
|
||||
final bool finished;
|
||||
final String? existingActivityEventId;
|
||||
|
||||
MessageActivityResponse({
|
||||
required this.activity,
|
||||
required this.finished,
|
||||
required this.existingActivityEventId,
|
||||
});
|
||||
|
||||
factory MessageActivityResponse.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -166,6 +215,7 @@ class MessageActivityResponse {
|
|||
)
|
||||
: null,
|
||||
finished: json['finished'] as bool,
|
||||
existingActivityEventId: json['existing_activity_event_id'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +223,7 @@ class MessageActivityResponse {
|
|||
return {
|
||||
'activity': activity?.toJson(),
|
||||
'finished': finished,
|
||||
'existing_activity_event_id': existingActivityEventId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.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/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class PracticeActivityRecordModel {
|
||||
final String? question;
|
||||
|
|
@ -57,12 +55,6 @@ class PracticeActivityRecordModel {
|
|||
return responses[responses.length - 1];
|
||||
}
|
||||
|
||||
ConstructUseTypeEnum get useType => latestResponse?.score != null
|
||||
? (latestResponse!.score > 0
|
||||
? ConstructUseTypeEnum.corPA
|
||||
: ConstructUseTypeEnum.incPA)
|
||||
: ConstructUseTypeEnum.unk;
|
||||
|
||||
bool hasTextResponse(String text) {
|
||||
return responses.any((element) => element.text == text);
|
||||
}
|
||||
|
|
@ -91,50 +83,50 @@ class PracticeActivityRecordModel {
|
|||
/// Returns a list of [OneConstructUse] objects representing the uses of the practice activity.
|
||||
///
|
||||
/// The [practiceActivity] parameter is the parent event, representing the activity itself.
|
||||
/// The [event] parameter is the record event, if available.
|
||||
/// The [metadata] parameter is the metadata for the construct use, used if the record event isn't available.
|
||||
///
|
||||
/// If [event] and [metadata] are both null, an empty list is returned.
|
||||
///
|
||||
/// The method iterates over the [tgtConstructs] of the [practiceActivity] and creates a [OneConstructUse] object for each construct.
|
||||
/// The method iterates over the [tgtConstructs] of the [practiceActivity] and creates a [OneConstructUse] object for each construct and useType.
|
||||
List<OneConstructUse> uses(
|
||||
PracticeActivityEvent practiceActivity, {
|
||||
Event? event,
|
||||
ConstructUseMetaData? metadata,
|
||||
}) {
|
||||
PracticeActivityModel practiceActivity,
|
||||
ConstructUseMetaData metadata,
|
||||
) {
|
||||
try {
|
||||
if (event == null && metadata == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<OneConstructUse> uses = [];
|
||||
final List<ConstructIdentifier> constructIds =
|
||||
practiceActivity.practiceActivity.tgtConstructs;
|
||||
|
||||
for (final construct in constructIds) {
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
lemma: construct.lemma,
|
||||
constructType: construct.type,
|
||||
useType: useType,
|
||||
//TODO - find form of construct within the message
|
||||
//this is related to the feature of highlighting the target construct in the message
|
||||
form: construct.lemma,
|
||||
metadata: ConstructUseMetaData(
|
||||
roomId: event?.roomId ?? metadata!.roomId,
|
||||
eventId: practiceActivity.parentMessageId,
|
||||
timeStamp: event?.originServerTs ?? metadata!.timeStamp,
|
||||
final uniqueResponses = responses.toSet();
|
||||
|
||||
final List<ConstructUseTypeEnum> useTypes =
|
||||
uniqueResponses.map((response) => response.useType).toList();
|
||||
|
||||
for (final construct in practiceActivity.tgtConstructs) {
|
||||
for (final useType in useTypes) {
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
lemma: construct.lemma,
|
||||
constructType: construct.type,
|
||||
useType: useType,
|
||||
//TODO - find form of construct within the message
|
||||
//this is related to the feature of highlighting the target construct in the message
|
||||
form: construct.lemma,
|
||||
metadata: metadata,
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return uses;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s, data: event?.toJson());
|
||||
rethrow;
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
'recordModel': toJson(),
|
||||
'practiceActivity': practiceActivity,
|
||||
'metadata': metadata,
|
||||
},
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +164,9 @@ class ActivityRecordResponse {
|
|||
required this.timestamp,
|
||||
});
|
||||
|
||||
ConstructUseTypeEnum get useType =>
|
||||
score > 0 ? ConstructUseTypeEnum.corPA : ConstructUseTypeEnum.incPA;
|
||||
|
||||
factory ActivityRecordResponse.fromJson(Map<String, dynamic> json) {
|
||||
return ActivityRecordResponse(
|
||||
text: json['text'] as String?,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,11 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
/// Whether the user has completed the activities needed to unlock the toolbar
|
||||
/// within this overlay 'session'. if they click out and come back in then
|
||||
/// we can give them some more activities to complete
|
||||
bool finishedActivitiesThisSession = false;
|
||||
int completedThisSession = 0;
|
||||
|
||||
bool get finishedActivitiesThisSession => completedThisSession >= needed;
|
||||
|
||||
late int activitiesLeftToComplete = needed;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -68,17 +72,29 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
duration: FluffyThemes.animationDuration,
|
||||
);
|
||||
|
||||
activitiesLeftToComplete =
|
||||
needed - widget._pangeaMessageEvent.numberOfActivitiesCompleted;
|
||||
|
||||
setInitialToolbarMode();
|
||||
}
|
||||
|
||||
int get activitiesLeftToComplete =>
|
||||
needed - widget._pangeaMessageEvent.numberOfActivitiesCompleted;
|
||||
|
||||
bool get isPracticeComplete => activitiesLeftToComplete <= 0;
|
||||
|
||||
/// When an activity is completed, we need to update the state
|
||||
/// and check if the toolbar should be unlocked
|
||||
void onActivityFinish() {
|
||||
if (!mounted) return;
|
||||
completedThisSession += 1;
|
||||
activitiesLeftToComplete -= 1;
|
||||
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() {
|
||||
debugPrint('Exiting practice flow');
|
||||
clearSelection();
|
||||
needed = 0;
|
||||
setInitialToolbarMode();
|
||||
setState(() {});
|
||||
|
|
@ -129,6 +145,10 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
void onClickOverlayMessageToken(
|
||||
PangeaToken token,
|
||||
) {
|
||||
if (toolbarMode == MessageMode.practiceActivity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if there's no selected span, then select the token
|
||||
if (_selectedSpan == null) {
|
||||
_selectedSpan = token.text;
|
||||
|
|
@ -150,7 +170,7 @@ class MessageOverlayController extends State<MessageSelectionOverlay>
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
void onNewActivity(PracticeActivityModel activity) {
|
||||
void setSelectedSpan(PracticeActivityModel activity) {
|
||||
final RelevantSpanDisplayDetails? span =
|
||||
activity.multipleChoice?.spanDisplayDetails;
|
||||
|
||||
|
|
|
|||
|
|
@ -250,7 +250,6 @@ class ToolbarButtonsState extends State<ToolbarButtons> {
|
|||
.toList();
|
||||
|
||||
static const double iconWidth = 36.0;
|
||||
double get progressWidth => widget.width / overlayController.needed;
|
||||
|
||||
MessageOverlayController get overlayController =>
|
||||
widget.messageToolbarController.widget.overLayController;
|
||||
|
|
@ -263,6 +262,8 @@ class ToolbarButtonsState extends State<ToolbarButtons> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double barWidth = widget.width - iconWidth;
|
||||
|
||||
if (widget
|
||||
.messageToolbarController.widget.pangeaMessageEvent.isAudioMessage) {
|
||||
return const SizedBox();
|
||||
|
|
@ -286,11 +287,13 @@ class ToolbarButtonsState extends State<ToolbarButtons> {
|
|||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
height: 12,
|
||||
width: min(
|
||||
widget.width,
|
||||
progressWidth *
|
||||
pangeaMessageEvent.numberOfActivitiesCompleted,
|
||||
),
|
||||
width: overlayController.isPracticeComplete
|
||||
? barWidth
|
||||
: min(
|
||||
barWidth,
|
||||
(barWidth / 3) *
|
||||
pangeaMessageEvent.numberOfActivitiesCompleted,
|
||||
),
|
||||
color: AppConfig.success,
|
||||
margin: const EdgeInsets.symmetric(horizontal: iconWidth / 2),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
widget.practiceCardController.currentCompletionRecord;
|
||||
|
||||
bool get isSubmitted =>
|
||||
widget.currentActivity?.userRecord?.record.latestResponse != null;
|
||||
widget.currentActivity?.latestUserRecord?.record.latestResponse != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -52,7 +52,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
/// Otherwise, it sets the current model to the user record's record and
|
||||
/// determines the selected choice index.
|
||||
void setCompletionRecord() {
|
||||
if (widget.currentActivity?.userRecord?.record == null) {
|
||||
if (widget.currentActivity?.latestUserRecord?.record == null) {
|
||||
widget.practiceCardController.setCompletionRecord(
|
||||
PracticeActivityRecordModel(
|
||||
question:
|
||||
|
|
@ -61,8 +61,8 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
);
|
||||
selectedChoiceIndex = null;
|
||||
} else {
|
||||
widget.practiceCardController
|
||||
.setCompletionRecord(widget.currentActivity!.userRecord!.record);
|
||||
widget.practiceCardController.setCompletionRecord(
|
||||
widget.currentActivity!.latestUserRecord!.record);
|
||||
selectedChoiceIndex = widget
|
||||
.currentActivity?.practiceActivity.multipleChoice!
|
||||
.choiceIndex(currentRecordModel!.latestResponse!.text!);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
|
|
@ -9,8 +8,8 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dar
|
|||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_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: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';
|
||||
|
|
@ -21,7 +20,6 @@ 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.dart';
|
||||
|
||||
/// The wrapper for practice activity content.
|
||||
/// Handles the activities associated with a message,
|
||||
|
|
@ -46,18 +44,14 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
PracticeActivityRecordModel? currentCompletionRecord;
|
||||
bool fetchingActivity = false;
|
||||
|
||||
List<TokenWithXP> targetTokens = [];
|
||||
TargetTokensController targetTokensController = TargetTokensController();
|
||||
|
||||
List<PracticeActivityEvent> get practiceActivities =>
|
||||
widget.pangeaMessageEvent.practiceActivities;
|
||||
|
||||
int get practiceEventIndex => practiceActivities.indexWhere(
|
||||
(activity) => activity.event.eventId == currentActivity?.event.eventId,
|
||||
);
|
||||
|
||||
/// TODO - @ggurdin - how can we start our processes (saving results and getting an activity)
|
||||
/// immediately after a correct choice but wait to display until x milliseconds after the choice is made AND
|
||||
/// we've received the new activity?
|
||||
// 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: 500);
|
||||
bool savoringTheJoy = false;
|
||||
Timer? joyTimer;
|
||||
|
|
@ -68,150 +62,100 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
initialize();
|
||||
}
|
||||
|
||||
void updateFetchingActivity(bool value) {
|
||||
@override
|
||||
void dispose() {
|
||||
joyTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateFetchingActivity(bool value) {
|
||||
if (fetchingActivity == value) return;
|
||||
setState(() => fetchingActivity = value);
|
||||
}
|
||||
|
||||
/// Get an activity to display.
|
||||
/// Show an uncompleted activity if there is one.
|
||||
/// Set target tokens.
|
||||
/// Get an existing activity if there is one.
|
||||
/// If not, get a new activity from the server.
|
||||
Future<void> initialize() async {
|
||||
targetTokens = await getTargetTokens();
|
||||
|
||||
currentActivity = _fetchExistingActivity() ?? await _fetchNewActivity();
|
||||
currentActivity =
|
||||
_fetchExistingIncompleteActivity() ?? await _fetchNewActivity();
|
||||
|
||||
currentActivity == null
|
||||
? widget.overlayController.exitPracticeFlow()
|
||||
: widget.overlayController
|
||||
.onNewActivity(currentActivity!.practiceActivity);
|
||||
.setSelectedSpan(currentActivity!.practiceActivity);
|
||||
}
|
||||
|
||||
// TODO - do more of a check for whether we have an appropropriate activity
|
||||
// if the user did the activity before but awhile ago and we don't have any
|
||||
// more target tokens, maybe we should give them the same activity again
|
||||
PracticeActivityEvent? _fetchExistingActivity() {
|
||||
final List<PracticeActivityEvent> incompleteActivities =
|
||||
practiceActivities.where((element) => !element.isComplete).toList();
|
||||
|
||||
final PracticeActivityEvent? existingActivity =
|
||||
incompleteActivities.isNotEmpty ? incompleteActivities.first : null;
|
||||
|
||||
return existingActivity != null &&
|
||||
existingActivity.practiceActivity !=
|
||||
currentActivity?.practiceActivity
|
||||
? existingActivity
|
||||
: null;
|
||||
}
|
||||
|
||||
Future<PracticeActivityEvent?> _fetchNewActivity() async {
|
||||
updateFetchingActivity(true);
|
||||
|
||||
if (targetTokens.isEmpty ||
|
||||
!pangeaController.languageController.languagesSet) {
|
||||
debugger(when: kDebugMode);
|
||||
updateFetchingActivity(false);
|
||||
PracticeActivityEvent? _fetchExistingIncompleteActivity() {
|
||||
if (practiceActivities.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final ourNewActivity =
|
||||
await pangeaController.practiceGenerationController.getPracticeActivity(
|
||||
MessageActivityRequest(
|
||||
userL1: pangeaController.languageController.userL1!.langCode,
|
||||
userL2: pangeaController.languageController.userL2!.langCode,
|
||||
messageText: representation!.text,
|
||||
tokensWithXP: targetTokens,
|
||||
messageId: widget.pangeaMessageEvent.eventId,
|
||||
),
|
||||
widget.pangeaMessageEvent,
|
||||
);
|
||||
final List<PracticeActivityEvent> incompleteActivities =
|
||||
practiceActivities.where((element) => !element.isComplete).toList();
|
||||
|
||||
/// Removes the target tokens of the new activity from the target tokens list.
|
||||
/// This avoids getting activities for the same token again, at least
|
||||
/// until the user exists the toolbar and re-enters it. By then, the
|
||||
/// analytics stream will have updated and the user will be able to get
|
||||
/// activity data for previously targeted tokens. This should then exclude
|
||||
/// the tokens that were targeted in previous activities based on xp and lastUsed.
|
||||
if (ourNewActivity?.practiceActivity.relevantSpanDisplayDetails != null) {
|
||||
targetTokens.removeWhere((token) {
|
||||
final RelevantSpanDisplayDetails span =
|
||||
ourNewActivity!.practiceActivity.relevantSpanDisplayDetails!;
|
||||
return token.token.text.offset >= span.offset &&
|
||||
token.token.text.offset + token.token.text.length <=
|
||||
span.offset + span.length;
|
||||
});
|
||||
}
|
||||
|
||||
updateFetchingActivity(false);
|
||||
|
||||
return ourNewActivity;
|
||||
// TODO - maybe check the user's xp for the tgtConstructs and decide if its relevant for them
|
||||
// however, maybe we'd like to go ahead and give them the activity to get some data on our xp?
|
||||
return incompleteActivities.firstOrNull;
|
||||
}
|
||||
|
||||
/// 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>> getTargetTokens() async {
|
||||
if (!mounted) {
|
||||
ErrorHandler.logError(
|
||||
m: 'getTargetTokens called when not mounted',
|
||||
s: StackTrace.current,
|
||||
Future<PracticeActivityEvent?> _fetchNewActivity() async {
|
||||
try {
|
||||
debugPrint('Fetching new 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 (!pangeaController.languageController.languagesSet) {
|
||||
debugger(when: kDebugMode);
|
||||
_updateFetchingActivity(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
debugger(when: kDebugMode);
|
||||
_updateFetchingActivity(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
final PracticeActivityEvent? ourNewActivity = await pangeaController
|
||||
.practiceGenerationController
|
||||
.getPracticeActivity(
|
||||
MessageActivityRequest(
|
||||
userL1: pangeaController.languageController.userL1!.langCode,
|
||||
userL2: pangeaController.languageController.userL2!.langCode,
|
||||
messageText: representation!.text,
|
||||
tokensWithXP: await targetTokensController.targetTokens(
|
||||
context,
|
||||
widget.pangeaMessageEvent,
|
||||
),
|
||||
messageId: widget.pangeaMessageEvent.eventId,
|
||||
existingActivities: practiceActivities
|
||||
.map((activity) => activity.activityRequestMetaData)
|
||||
.toList(),
|
||||
),
|
||||
widget.pangeaMessageEvent,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// we're just going to set this once per session
|
||||
// we remove the target tokens when we get a new activity
|
||||
if (targetTokens.isNotEmpty) return targetTokens;
|
||||
_updateFetchingActivity(false);
|
||||
|
||||
if (representation == null) {
|
||||
return ourNewActivity;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
m: 'Failed to get new activity',
|
||||
data: {
|
||||
'activity': currentActivity,
|
||||
'record': currentCompletionRecord,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
final tokens = await representation?.tokensGlobal(context);
|
||||
|
||||
if (tokens == null || tokens.isEmpty) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
}
|
||||
|
||||
var constructUses =
|
||||
MatrixState.pangeaController.analytics.analyticsStream.value;
|
||||
|
||||
if (constructUses == null || constructUses.isEmpty) {
|
||||
constructUses = [];
|
||||
//@gurdin - this is happening for me with a brand-new user. however, in this case, constructUses should be empty list
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
|
||||
final ConstructListModel constructList = ConstructListModel(
|
||||
uses: constructUses,
|
||||
type: null,
|
||||
);
|
||||
|
||||
final List<TokenWithXP> tokenCounts = [];
|
||||
|
||||
// TODO - add morph constructs to this list as well
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
//don't bother with tokens that we don't save to vocab
|
||||
if (!tokens[i].lemma.saveVocab) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tokenCounts.add(tokens[i].emptyTokenWithXP);
|
||||
|
||||
for (final construct in tokenCounts.last.constructs) {
|
||||
final constructUseModel = constructList.getConstructUses(
|
||||
construct.id.lemma,
|
||||
construct.id.type,
|
||||
);
|
||||
if (constructUseModel != null) {
|
||||
construct.xp = constructUseModel.points;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokenCounts.sort((a, b) => a.xp.compareTo(b.xp));
|
||||
|
||||
return tokenCounts;
|
||||
}
|
||||
|
||||
void setCompletionRecord(PracticeActivityRecordModel? recordModel) {
|
||||
|
|
@ -219,7 +163,7 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
}
|
||||
|
||||
/// future that simply waits for the appropriate time to savor the joy
|
||||
Future<void> savorTheJoy() async {
|
||||
Future<void> _savorTheJoy() async {
|
||||
joyTimer?.cancel();
|
||||
if (savoringTheJoy) return;
|
||||
savoringTheJoy = true;
|
||||
|
|
@ -229,10 +173,10 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
});
|
||||
}
|
||||
|
||||
/// Sends the current record model and activity to the server.
|
||||
/// If either the currentRecordModel or currentActivity is null, the method returns early.
|
||||
/// If the currentActivity is the last activity, the method sets the appropriate flag to true.
|
||||
/// If the currentActivity is not the last activity, the method fetches a new activity.
|
||||
/// Called when the user finishes an activity.
|
||||
/// 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 {
|
||||
try {
|
||||
if (currentCompletionRecord == null || currentActivity == null) {
|
||||
|
|
@ -241,45 +185,63 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
}
|
||||
|
||||
// start joy timer
|
||||
savorTheJoy();
|
||||
_savorTheJoy();
|
||||
|
||||
// if this is the last activity, set the flag to true
|
||||
// so we can give them some kudos
|
||||
if (widget.overlayController.activitiesLeftToComplete == 1) {
|
||||
widget.overlayController.finishedActivitiesThisSession = true;
|
||||
}
|
||||
|
||||
final Event? event = await MatrixState
|
||||
.pangeaController.activityRecordController
|
||||
.send(currentCompletionRecord!, currentActivity!);
|
||||
|
||||
MatrixState.pangeaController.myAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
eventId: widget.pangeaMessageEvent.eventId,
|
||||
eventType: PangeaEventTypes.activityRecord,
|
||||
roomId: event!.room.id,
|
||||
practiceActivity: currentActivity!,
|
||||
recordModel: currentCompletionRecord!,
|
||||
final uses = currentCompletionRecord!.uses(
|
||||
currentActivity!.practiceActivity,
|
||||
ConstructUseMetaData(
|
||||
roomId: widget.pangeaMessageEvent.room.id,
|
||||
timeStamp: DateTime.now(),
|
||||
),
|
||||
);
|
||||
|
||||
if (!widget.overlayController.finishedActivitiesThisSession) {
|
||||
currentActivity = await _fetchNewActivity();
|
||||
// update the target tokens with the new construct uses
|
||||
targetTokensController.updateTokensWithConstructs(
|
||||
uses,
|
||||
context,
|
||||
widget.pangeaMessageEvent,
|
||||
);
|
||||
|
||||
currentActivity == null
|
||||
? widget.overlayController.exitPracticeFlow()
|
||||
: widget.overlayController
|
||||
.onNewActivity(currentActivity!.practiceActivity);
|
||||
} else {
|
||||
updateFetchingActivity(false);
|
||||
widget.overlayController.setState(() {});
|
||||
}
|
||||
MatrixState.pangeaController.myAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
// note - this maybe should be the activity event id
|
||||
eventId: widget.pangeaMessageEvent.eventId,
|
||||
roomId: widget.pangeaMessageEvent.room.id,
|
||||
constructs: uses,
|
||||
),
|
||||
);
|
||||
|
||||
// save the record without awaiting to avoid blocking the UI
|
||||
// send a copy of the activity record to make sure its not overwritten by
|
||||
// the new activity
|
||||
MatrixState.pangeaController.activityRecordController
|
||||
.send(currentCompletionRecord!, currentActivity!)
|
||||
.catchError(
|
||||
(e, s) => ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
m: 'Failed to save record',
|
||||
data: {
|
||||
'record': currentCompletionRecord?.toJson(),
|
||||
'activity': currentActivity?.practiceActivity.toJson(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
widget.overlayController.onActivityFinish();
|
||||
|
||||
currentActivity = await _fetchNewActivity();
|
||||
|
||||
currentActivity == null
|
||||
? widget.overlayController.exitPracticeFlow()
|
||||
: widget.overlayController
|
||||
.setSelectedSpan(currentActivity!.practiceActivity);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
m: 'Failed to send record for activity',
|
||||
m: 'Failed to get new activity',
|
||||
data: {
|
||||
'activity': currentActivity,
|
||||
'record': currentCompletionRecord,
|
||||
|
|
@ -340,6 +302,9 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint(
|
||||
'Building practice activity card with ${widget.overlayController.activitiesLeftToComplete} activities left to complete',
|
||||
);
|
||||
if (userMessage != null) {
|
||||
return Center(
|
||||
child: Container(
|
||||
|
|
@ -385,3 +350,92 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
BuildContext context,
|
||||
PangeaMessageEvent pangeaMessageEvent,
|
||||
) async {
|
||||
if (_targetTokens != null) {
|
||||
return _targetTokens!;
|
||||
}
|
||||
|
||||
_targetTokens = await _initialize(context, pangeaMessageEvent);
|
||||
|
||||
await updateTokensWithConstructs(
|
||||
MatrixState.pangeaController.analytics.analyticsStream.value ?? [],
|
||||
context,
|
||||
pangeaMessageEvent,
|
||||
);
|
||||
|
||||
return _targetTokens!;
|
||||
}
|
||||
|
||||
Future<List<TokenWithXP>> _initialize(
|
||||
BuildContext context,
|
||||
PangeaMessageEvent pangeaMessageEvent,
|
||||
) async {
|
||||
if (!context.mounted) {
|
||||
ErrorHandler.logError(
|
||||
m: 'getTargetTokens called when not mounted',
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return _targetTokens = [];
|
||||
}
|
||||
|
||||
final tokens = await pangeaMessageEvent
|
||||
.representationByLanguage(pangeaMessageEvent.messageDisplayLangCode)
|
||||
?.tokensGlobal(context);
|
||||
|
||||
if (tokens == null || tokens.isEmpty) {
|
||||
debugger(when: kDebugMode);
|
||||
return _targetTokens = [];
|
||||
}
|
||||
|
||||
_targetTokens = [];
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
//don't bother with tokens that we don't save to vocab
|
||||
if (!tokens[i].lemma.saveVocab) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_targetTokens!.add(tokens[i].emptyTokenWithXP);
|
||||
}
|
||||
|
||||
return _targetTokens!;
|
||||
}
|
||||
|
||||
Future<void> updateTokensWithConstructs(
|
||||
List<OneConstructUse> constructUses,
|
||||
context,
|
||||
pangeaMessageEvent,
|
||||
) async {
|
||||
final ConstructListModel constructList = ConstructListModel(
|
||||
uses: constructUses,
|
||||
type: null,
|
||||
);
|
||||
|
||||
_targetTokens ??= await _initialize(context, pangeaMessageEvent);
|
||||
|
||||
for (final token in _targetTokens!) {
|
||||
for (final construct in token.constructs) {
|
||||
final constructUseModel = constructList.getConstructUses(
|
||||
construct.id.lemma,
|
||||
construct.id.type,
|
||||
);
|
||||
if (constructUseModel != null) {
|
||||
construct.xp = constructUseModel.points;
|
||||
construct.lastUsed = constructUseModel.lastUsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue