You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
347 lines
11 KiB
Dart
347 lines
11 KiB
Dart
// record the options that the user selected
|
|
// note that this is not the same as the correct answer
|
|
// the user might have selected multiple options before
|
|
// finding the answer
|
|
|
|
import 'dart:developer';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
|
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
|
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
|
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
|
import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
|
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
|
|
import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart';
|
|
import 'package:fluffychat/pangea/practice_activities/practice_record_repo.dart';
|
|
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
|
|
|
|
class PracticeRecord {
|
|
late DateTime createdAt;
|
|
late List<ActivityRecordResponse> responses;
|
|
|
|
PracticeRecord({
|
|
List<ActivityRecordResponse>? responses,
|
|
DateTime? timestamp,
|
|
}) {
|
|
createdAt = timestamp ?? DateTime.now();
|
|
if (responses == null) {
|
|
this.responses = List<ActivityRecordResponse>.empty(growable: true);
|
|
} else {
|
|
this.responses = responses;
|
|
}
|
|
}
|
|
|
|
factory PracticeRecord.fromJson(
|
|
Map<String, dynamic> json,
|
|
) {
|
|
return PracticeRecord(
|
|
responses: (json['responses'] as List)
|
|
.map(
|
|
(e) => ActivityRecordResponse.fromJson(e as Map<String, dynamic>),
|
|
)
|
|
.toList(),
|
|
timestamp: json['createdAt'] != null
|
|
? DateTime.parse(json['createdAt'] as String)
|
|
: null,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'responses': responses.map((e) => e.toJson()).toList(),
|
|
'createdAt': createdAt.toIso8601String(),
|
|
};
|
|
}
|
|
|
|
int get completeResponses =>
|
|
responses.where((element) => element.isCorrect).length;
|
|
|
|
/// get the latest response index according to the response timeStamp
|
|
/// sort the responses by timestamp and get the index of the last response
|
|
ActivityRecordResponse? get latestResponse {
|
|
if (responses.isEmpty) {
|
|
return null;
|
|
}
|
|
responses.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
|
return responses[responses.length - 1];
|
|
}
|
|
|
|
bool hasTextResponse(String text) {
|
|
return responses.any((element) => element.text == text);
|
|
}
|
|
|
|
bool alreadyHasMatchResponse(
|
|
ConstructIdentifier cId,
|
|
String text,
|
|
) {
|
|
return responses.any(
|
|
(element) => element.cId == cId && element.text == text,
|
|
);
|
|
}
|
|
|
|
/// [target] needed for saving the record, little funky
|
|
/// [cId] identifies the construct in the case of match activities which have multiple
|
|
/// [text] is the user's response
|
|
/// [audioBytes] is the user's audio response
|
|
/// [imageBytes] is the user's image response
|
|
/// [score] > 0 means correct, otherwise is incorrect
|
|
void addResponse({
|
|
required ConstructIdentifier cId,
|
|
required PracticeTarget target,
|
|
String? text,
|
|
Uint8List? audioBytes,
|
|
Uint8List? imageBytes,
|
|
required double score,
|
|
}) {
|
|
try {
|
|
if (text == null && audioBytes == null && imageBytes == null) {
|
|
debugger(when: kDebugMode);
|
|
ErrorHandler.logError(
|
|
m: "No response data provided",
|
|
data: {
|
|
'cId': cId.toJson(),
|
|
'text': text,
|
|
'audioBytes': audioBytes,
|
|
'imageBytes': imageBytes,
|
|
'score': score,
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
responses.add(
|
|
ActivityRecordResponse(
|
|
cId: cId,
|
|
text: text,
|
|
audioBytes: audioBytes,
|
|
imageBytes: imageBytes,
|
|
timestamp: DateTime.now(),
|
|
score: score,
|
|
),
|
|
);
|
|
debugPrint("responses: ${responses.map((r) => r.toJson())}");
|
|
|
|
PracticeRecordRepo.save(target, this);
|
|
} catch (e) {
|
|
debugger(when: kDebugMode);
|
|
}
|
|
}
|
|
|
|
void clearResponses() {
|
|
responses.clear();
|
|
}
|
|
|
|
/// 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 [metadata] parameter is the metadata for the construct use, used if the record event isn't available.
|
|
///
|
|
/// The method iterates over the [responses] to get [OneConstructUse] objects for each
|
|
List<OneConstructUse> usesForAllResponses(
|
|
PracticeActivityModel practiceActivity,
|
|
ConstructUseMetaData metadata,
|
|
) =>
|
|
responses
|
|
.toSet()
|
|
.expand(
|
|
(response) => response.toUses(practiceActivity, metadata),
|
|
)
|
|
.toList();
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (identical(this, other)) return true;
|
|
|
|
return other is PracticeRecord &&
|
|
other.responses.length == responses.length &&
|
|
List.generate(
|
|
responses.length,
|
|
(index) => responses[index] == other.responses[index],
|
|
).every((element) => element);
|
|
}
|
|
|
|
@override
|
|
int get hashCode => responses.hashCode;
|
|
}
|
|
|
|
class ActivityRecordResponse {
|
|
/// the cId of the construct that the user attached their response to
|
|
/// ie. in the "I like the dog" if the user erroneously attaches a dog emoji to the word like
|
|
/// then the cId is that of 'like
|
|
ConstructIdentifier cId;
|
|
// the user's response
|
|
// has nullable string, nullable audio bytes, nullable image bytes, and timestamp
|
|
final String? text;
|
|
final Uint8List? audioBytes;
|
|
final Uint8List? imageBytes;
|
|
final DateTime timestamp;
|
|
final double score;
|
|
|
|
ActivityRecordResponse({
|
|
required this.cId,
|
|
this.text,
|
|
this.audioBytes,
|
|
this.imageBytes,
|
|
required this.score,
|
|
required this.timestamp,
|
|
});
|
|
|
|
bool get isCorrect => score > 0;
|
|
|
|
//TODO - differentiate into different activity types
|
|
ConstructUseTypeEnum useType(ActivityTypeEnum aType) =>
|
|
isCorrect ? aType.correctUse : aType.incorrectUse;
|
|
|
|
// for each target construct create a OneConstructUse object
|
|
List<OneConstructUse> toUses(
|
|
PracticeActivityModel practiceActivity,
|
|
ConstructUseMetaData metadata,
|
|
) {
|
|
// if the emoji is already set, don't give points
|
|
// IMPORTANT: This assumes that scoring is happening before saving of the user's emoji choice.
|
|
if (practiceActivity.activityType == ActivityTypeEnum.emoji &&
|
|
practiceActivity.targetTokens.first.getEmoji().isNotEmpty) {
|
|
return [];
|
|
}
|
|
|
|
if (practiceActivity.targetTokens.isEmpty) {
|
|
debugger(when: kDebugMode);
|
|
ErrorHandler.logError(
|
|
m: "null targetTokens in practice activity",
|
|
data: practiceActivity.toJson(),
|
|
);
|
|
return [];
|
|
}
|
|
|
|
switch (practiceActivity.activityType) {
|
|
case ActivityTypeEnum.emoji:
|
|
case ActivityTypeEnum.wordMeaning:
|
|
case ActivityTypeEnum.wordFocusListening:
|
|
case ActivityTypeEnum.lemmaId:
|
|
final token = practiceActivity.targetTokens.first;
|
|
final constructUseType = useType(practiceActivity.activityType);
|
|
return [
|
|
OneConstructUse(
|
|
lemma: token.lemma.text,
|
|
form: token.text.content,
|
|
constructType: ConstructTypeEnum.vocab,
|
|
useType: constructUseType,
|
|
metadata: metadata,
|
|
category: token.pos,
|
|
xp: constructUseType.pointValue,
|
|
),
|
|
];
|
|
case ActivityTypeEnum.messageMeaning:
|
|
final constructUseType = useType(practiceActivity.activityType);
|
|
return practiceActivity.targetTokens
|
|
.expand(
|
|
(t) => t.allUses(
|
|
constructUseType,
|
|
metadata,
|
|
constructUseType.pointValue,
|
|
),
|
|
)
|
|
.toList();
|
|
case ActivityTypeEnum.hiddenWordListening:
|
|
final constructUseType = useType(practiceActivity.activityType);
|
|
return practiceActivity.targetTokens
|
|
.map(
|
|
(token) => OneConstructUse(
|
|
lemma: token.lemma.text,
|
|
form: token.text.content,
|
|
constructType: ConstructTypeEnum.vocab,
|
|
useType: constructUseType,
|
|
metadata: metadata,
|
|
category: token.pos,
|
|
xp: constructUseType.pointValue,
|
|
),
|
|
)
|
|
.toList();
|
|
case ActivityTypeEnum.morphId:
|
|
if (practiceActivity.morphFeature == null) {
|
|
debugger(when: kDebugMode);
|
|
ErrorHandler.logError(
|
|
m: "null morphFeature in morph activity",
|
|
data: practiceActivity.toJson(),
|
|
);
|
|
return [];
|
|
}
|
|
return practiceActivity.targetTokens
|
|
.map(
|
|
(t) {
|
|
final tag = t.getMorphTag(practiceActivity.morphFeature!);
|
|
if (tag == null) {
|
|
debugger(when: kDebugMode);
|
|
ErrorHandler.logError(
|
|
m: "null tag in morph activity",
|
|
data: practiceActivity.toJson(),
|
|
);
|
|
return null;
|
|
}
|
|
final constructUseType = useType(practiceActivity.activityType);
|
|
return OneConstructUse(
|
|
lemma: tag,
|
|
form: practiceActivity.targetTokens.first.text.content,
|
|
constructType: ConstructTypeEnum.morph,
|
|
useType: constructUseType,
|
|
metadata: metadata,
|
|
category: practiceActivity.morphFeature!,
|
|
xp: constructUseType.pointValue,
|
|
);
|
|
},
|
|
)
|
|
.where((c) => c != null)
|
|
.cast<OneConstructUse>()
|
|
.toList();
|
|
}
|
|
}
|
|
|
|
factory ActivityRecordResponse.fromJson(Map<String, dynamic> json) {
|
|
return ActivityRecordResponse(
|
|
cId: ConstructIdentifier.fromJson(json['cId'] as Map<String, dynamic>),
|
|
text: json['text'] as String?,
|
|
audioBytes: json['audio'] as Uint8List?,
|
|
imageBytes: json['image'] as Uint8List?,
|
|
timestamp: DateTime.parse(json['timestamp'] as String),
|
|
// this has a default of 1 to make this backwards compatible
|
|
// score was added later and is not present in all records
|
|
// currently saved to Matrix
|
|
score: json['score'] ?? 1.0,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'cId': cId.toJson(),
|
|
'text': text,
|
|
'audio': audioBytes,
|
|
'image': imageBytes,
|
|
'timestamp': timestamp.toIso8601String(),
|
|
'score': score.toInt(),
|
|
};
|
|
}
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
if (identical(this, other)) return true;
|
|
|
|
return other is ActivityRecordResponse &&
|
|
other.text == text &&
|
|
other.audioBytes == audioBytes &&
|
|
other.imageBytes == imageBytes &&
|
|
other.timestamp == timestamp &&
|
|
other.score == score &&
|
|
other.cId == cId;
|
|
}
|
|
|
|
@override
|
|
int get hashCode =>
|
|
text.hashCode ^
|
|
audioBytes.hashCode ^
|
|
imageBytes.hashCode ^
|
|
timestamp.hashCode ^
|
|
score.hashCode ^
|
|
cId.hashCode;
|
|
}
|