rough draft complete

This commit is contained in:
William Jordan-Cooley 2024-06-28 15:30:57 -04:00
parent 8ceb7851e5
commit 5c8666b3e2
19 changed files with 484 additions and 474 deletions

View file

@ -641,7 +641,7 @@ class AnalyticsController extends BaseController {
List<ConstructAnalyticsEvent>? getConstructsLocal({
required TimeSpan timeSpan,
required ConstructType constructType,
required ConstructTypeEnum constructType,
required AnalyticsSelected defaultSelected,
AnalyticsSelected? selected,
DateTime? lastUpdated,
@ -669,7 +669,7 @@ class AnalyticsController extends BaseController {
}
void cacheConstructs({
required ConstructType constructType,
required ConstructTypeEnum constructType,
required List<ConstructAnalyticsEvent> events,
required AnalyticsSelected defaultSelected,
AnalyticsSelected? selected,
@ -687,7 +687,7 @@ class AnalyticsController extends BaseController {
Future<List<ConstructAnalyticsEvent>> getMyConstructs({
required AnalyticsSelected defaultSelected,
required ConstructType constructType,
required ConstructTypeEnum constructType,
AnalyticsSelected? selected,
}) async {
final List<ConstructAnalyticsEvent> unfilteredConstructs =
@ -706,7 +706,7 @@ class AnalyticsController extends BaseController {
}
Future<List<ConstructAnalyticsEvent>> getSpaceConstructs({
required ConstructType constructType,
required ConstructTypeEnum constructType,
required Room space,
required AnalyticsSelected defaultSelected,
AnalyticsSelected? selected,
@ -768,7 +768,7 @@ class AnalyticsController extends BaseController {
}
Future<List<ConstructAnalyticsEvent>?> getConstructs({
required ConstructType constructType,
required ConstructTypeEnum constructType,
required AnalyticsSelected defaultSelected,
AnalyticsSelected? selected,
bool removeIT = true,
@ -898,7 +898,7 @@ abstract class CacheEntry {
}
class ConstructCacheEntry extends CacheEntry {
final ConstructType type;
final ConstructTypeEnum type;
final List<ConstructAnalyticsEvent> events;
ConstructCacheEntry({

View file

@ -1,17 +1,13 @@
import 'dart:async';
import 'dart:developer';
<<<<<<< Updated upstream
import 'package:fluffychat/pangea/constants/language_constants.dart';
=======
>>>>>>> Stashed changes
import 'package:fluffychat/pangea/constants/local.key.dart';
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
@ -196,9 +192,8 @@ class MyAnalyticsController {
String? get userL2 => _pangeaController.languageController.activeL2Code();
// top level analytics sending function. Send analytics
// for each type of analytics event
// to each of the applicable analytics rooms
/// top level analytics sending function. Gather recent messages and activity records,
/// convert them into the correct formats, and send them to the analytics room
Future<void> _updateAnalytics() async {
// if missing important info, don't send analytics
if (userL2 == null || _client.userID == null) {
@ -206,151 +201,108 @@ class MyAnalyticsController {
return;
}
// get the last updated time for each analytics room
// and the least recent update, which will be used to determine
// how far to go back in the chat history to get messages
final Map<String, DateTime?> lastUpdatedMap = await _pangeaController
.matrixState.client
.allAnalyticsRoomsLastUpdated();
final List<DateTime> lastUpdates = lastUpdatedMap.values
.where((lastUpdate) => lastUpdate != null)
.cast<DateTime>()
.toList();
/// Get the last time that analytics to for current target language
/// were updated. This my present a problem is the user has analytics
/// rooms for multiple languages, and a non-target language was updated
/// less recently than the target language. In this case, some data may
/// be missing, but a case like that seems relatively rare, and could
/// result in unnecessaily going too far back in the chat history
DateTime? l2AnalyticsLastUpdated = lastUpdatedMap[userL2];
if (l2AnalyticsLastUpdated == null) {
/// if the target language has never been updated, use the least
/// recent update time
lastUpdates.sort((a, b) => a.compareTo(b));
l2AnalyticsLastUpdated =
lastUpdates.isNotEmpty ? lastUpdates.first : null;
}
final List<Room> chats = await _client.chatsImAStudentIn;
final List<PangeaMessageEvent> recentMsgs =
await _getMessagesWithUnsavedAnalytics(
l2AnalyticsLastUpdated,
chats,
);
final List<ActivityRecordResponse> recentActivities =
await getRecentActivities(userL2!, l2AnalyticsLastUpdated, chats);
// FOR DISCUSSION:
// we want to make sure we save something for every message send
// however, we're currently saving analytics for messages not in the userL2
// based on bad language detection results. maybe it would be better to
// save the analytics for these messages in the userL2 analytics room, but
// with useType of unknown
// analytics room for the user and current target language
final Room analyticsRoom = await _client.getMyAnalyticsRoom(userL2!);
// if there is no analytics room for this langCode, then user hadn't sent
// message in this language at the time of the last analytics update
// so fallback to the least recent update time
final DateTime? lastUpdated =
lastUpdatedMap[analyticsRoom.id] ?? l2AnalyticsLastUpdated;
// final String msgLangCode = (msg.originalSent?.langCode != null &&
// msg.originalSent?.langCode != LanguageKeys.unknownLanguage)
// ? msg.originalSent!.langCode
// : userL2;
// finally, send the analytics events to the analytics room
await _sendAnalyticsEvents(
analyticsRoom,
recentMsgs,
lastUpdated,
recentActivities,
// get the last time analytics were updated for this room
final DateTime? l2AnalyticsLastUpdated =
await analyticsRoom.analyticsLastUpdated(
PangeaEventTypes.summaryAnalytics,
_client.userID!,
);
}
Future<List<ActivityRecordResponse>> getRecentActivities(
String userL2,
DateTime? lastUpdated,
List<Room> chats,
) async {
// all chats in which user is a student
final List<Room> chats = await _client.chatsImAStudentIn;
// get the recent message events and activity records for each chat
final List<Future<List<Event>>> recentMsgFutures = [];
final List<Future<List<Event>>> recentActivityFutures = [];
for (final Room chat in chats) {
chat.getEventsBySender(
type: EventTypes.Message,
sender: _client.userID!,
since: l2AnalyticsLastUpdated,
);
recentActivityFutures.add(
chat.getEventsBySender(
type: PangeaEventTypes.activityRecord,
sender: _client.userID!,
since: lastUpdated,
since: l2AnalyticsLastUpdated,
),
);
}
final List<List<Event>> recentActivityLists =
await Future.wait(recentActivityFutures);
final List<List<Event>> recentMsgs =
(await Future.wait(recentMsgFutures)).toList();
final List<PracticeActivityRecordEvent> recentActivityReconds =
(await Future.wait(recentActivityFutures))
.expand((e) => e)
.map((event) => PracticeActivityRecordEvent(event: event))
.toList();
return recentActivityLists
.expand((e) => e)
.map((e) => ActivityRecordResponse.fromJson(e.content))
.toList();
}
// get the timelines for each chat
final List<Future<Timeline>> timelineFutures = [];
for (final chat in chats) {
timelineFutures.add(chat.getTimeline());
}
final List<Timeline> timelines = await Future.wait(timelineFutures);
final Map<String, Timeline> timelineMap =
Map.fromIterables(chats.map((e) => e.id), timelines);
/// Returns the new messages that have not yet been saved to analytics.
/// The keys in the map correspond to different categories or groups of messages,
/// while the values are lists of [PangeaMessageEvent] objects belonging to each category.
Future<List<PangeaMessageEvent>> _getMessagesWithUnsavedAnalytics(
DateTime? since,
List<Room> chats,
) async {
// get the recent messages for each chat
final List<Future<List<PangeaMessageEvent>>> futures = [];
for (final Room chat in chats) {
futures.add(
chat.myMessageEventsInChat(
since: since,
),
//convert into PangeaMessageEvents
final List<List<PangeaMessageEvent>> recentPangeaMessageEvents = [];
for (final (index, eventList) in recentMsgs.indexed) {
recentPangeaMessageEvents.add(
eventList
.map(
(event) => PangeaMessageEvent(
event: event,
timeline: timelines[index],
ownMessage: true,
),
)
.toList(),
);
}
final List<List<PangeaMessageEvent>> recentMsgLists =
await Future.wait(futures);
// flatten the list of lists of messages
return recentMsgLists.expand((e) => e).toList();
}
final List<PangeaMessageEvent> allRecentMessages =
recentPangeaMessageEvents.expand((e) => e).toList();
Future<void> _sendAnalyticsEvents(
Room analyticsRoom,
List<PangeaMessageEvent> recentMsgs,
DateTime? lastUpdated,
List<ActivityRecordResponse> recentActivities,
) async {
final List<RecentMessageRecord> summaryContent =
SummaryAnalyticsModel.formatSummaryContent(allRecentMessages);
// if there's new content to be sent, or if lastUpdated hasn't been
// set yet for this room, send the analytics events
if (summaryContent.isNotEmpty || l2AnalyticsLastUpdated == null) {
await analyticsRoom.sendSummaryAnalyticsEvent(
summaryContent,
);
}
// get constructs for messages
final List<OneConstructUse> constructContent = [];
for (final PangeaMessageEvent message in allRecentMessages) {
constructContent.addAll(message.allConstructUses);
}
if (recentMsgs.isNotEmpty) {
// remove messages that were sent before the last update
// format the analytics data
final List<RecentMessageRecord> summaryContent =
SummaryAnalyticsModel.formatSummaryContent(recentMsgs);
// if there's new content to be sent, or if lastUpdated hasn't been
// set yet for this room, send the analytics events
if (summaryContent.isNotEmpty || lastUpdated == null) {
await analyticsRoom.sendSummaryAnalyticsEvent(
summaryContent,
// get constructs for practice activities
final List<Future<List<OneConstructUse>>> constructFutures = [];
for (final PracticeActivityRecordEvent activity in recentActivityReconds) {
final Timeline? timeline = timelineMap[activity.event.roomId!];
if (timeline == null) {
debugger(when: kDebugMode);
ErrorHandler.logError(
m: "PracticeActivityRecordEvent has null timeline",
data: activity.event.toJson(),
);
continue;
}
constructContent
.addAll(ConstructAnalyticsModel.formatConstructsContent(recentMsgs));
constructFutures.add(activity.uses(timeline));
}
final List<List<OneConstructUse>> constructLists =
await Future.wait(constructFutures);
if (recentActivities.isNotEmpty) {
// TODO - Concert recentActivities into list of constructUse objects.
// First, We need to get related practiceActivityEvent from timeline in order to get its related constructs. Alternatively we
// could search for completed practice activities and see which have been completed by the user.
// It's not clear which is the best approach at the moment and we should consider both.
}
constructContent.addAll(constructLists.expand((e) => e));
debugger(when: kDebugMode);
await analyticsRoom.sendConstructsEvent(
constructContent,

View file

@ -88,7 +88,7 @@ class PracticeGenerationController {
PracticeActivityModel dummyModel(PangeaMessageEvent event) =>
PracticeActivityModel(
tgtConstructs: [
ConstructIdentifier(lemma: "be", type: ConstructType.vocab),
ConstructIdentifier(lemma: "be", type: ConstructTypeEnum.vocab),
],
activityType: ActivityTypeEnum.multipleChoice,
langCode: event.messageDisplayLangCode,

View file

@ -1,30 +1,30 @@
enum ConstructType {
enum ConstructTypeEnum {
grammar,
vocab,
}
extension ConstructExtension on ConstructType {
extension ConstructExtension on ConstructTypeEnum {
String get string {
switch (this) {
case ConstructType.grammar:
case ConstructTypeEnum.grammar:
return 'grammar';
case ConstructType.vocab:
case ConstructTypeEnum.vocab:
return 'vocab';
}
}
}
class ConstructTypeUtil {
static ConstructType fromString(String? string) {
static ConstructTypeEnum fromString(String? string) {
switch (string) {
case 'g':
case 'grammar':
return ConstructType.grammar;
return ConstructTypeEnum.grammar;
case 'v':
case 'vocab':
return ConstructType.vocab;
return ConstructTypeEnum.vocab;
default:
return ConstructType.vocab;
return ConstructTypeEnum.vocab;
}
}
}

View file

@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
enum ConstructUseTypeEnum {
/// produced in chat by user, igc was run, and we've judged it to be a correct use
wa,
/// produced in chat by user, igc was run, and we've judged it to be a incorrect use
/// Note: if the IGC match is ignored, this is not counted as an incorrect use
ga,
/// produced in chat by user and igc was not run
unk,
/// selected correctly in IT flow
corIt,
/// encountered as IT distractor and correctly ignored it
ignIt,
/// encountered as it distractor and selected it
incIt,
/// encountered in igc match and ignored match
ignIGC,
/// selected correctly in IGC flow
corIGC,
/// encountered as distractor in IGC flow and selected it
incIGC,
/// selected correctly in practice activity flow
corPA,
/// was target construct in practice activity but user did not select correctly
incPA,
}
extension ConstructUseTypeExtension on ConstructUseTypeEnum {
String get string {
switch (this) {
case ConstructUseTypeEnum.ga:
return 'ga';
case ConstructUseTypeEnum.wa:
return 'wa';
case ConstructUseTypeEnum.corIt:
return 'corIt';
case ConstructUseTypeEnum.incIt:
return 'incIt';
case ConstructUseTypeEnum.ignIt:
return 'ignIt';
case ConstructUseTypeEnum.ignIGC:
return 'ignIGC';
case ConstructUseTypeEnum.corIGC:
return 'corIGC';
case ConstructUseTypeEnum.incIGC:
return 'incIGC';
case ConstructUseTypeEnum.unk:
return 'unk';
case ConstructUseTypeEnum.corPA:
return 'corPA';
case ConstructUseTypeEnum.incPA:
return 'incPA';
}
}
IconData get icon {
switch (this) {
case ConstructUseTypeEnum.ga:
return Icons.check;
case ConstructUseTypeEnum.wa:
return Icons.thumb_up_sharp;
case ConstructUseTypeEnum.corIt:
return Icons.check;
case ConstructUseTypeEnum.incIt:
return Icons.close;
case ConstructUseTypeEnum.ignIt:
return Icons.close;
case ConstructUseTypeEnum.ignIGC:
return Icons.close;
case ConstructUseTypeEnum.corIGC:
return Icons.check;
case ConstructUseTypeEnum.incIGC:
return Icons.close;
case ConstructUseTypeEnum.corPA:
return Icons.check;
case ConstructUseTypeEnum.incPA:
return Icons.close;
case ConstructUseTypeEnum.unk:
return Icons.help;
}
}
}

View file

@ -426,32 +426,6 @@ extension EventsRoomExtension on Room {
// }
// }
Future<List<PangeaMessageEvent>> myMessageEventsInChat({
DateTime? since,
}) async {
try {
final List<Event> msgEvents = await getEventsBySender(
type: EventTypes.Message,
sender: client.userID!,
since: since,
);
final Timeline timeline = await getTimeline();
return msgEvents
.where((event) => (event.content['msgtype'] == MessageTypes.Text))
.map((event) {
return PangeaMessageEvent(
event: event,
timeline: timeline,
ownMessage: true,
);
}).toList();
} catch (err, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: err, s: s);
return [];
}
}
// fetch event of a certain type by a certain sender
// since a certain time or up to a certain amount
Future<List<Event>> getEventsBySender({

View file

@ -2,14 +2,20 @@ import 'dart:convert';
import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
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/constructs_model.dart';
import 'package:fluffychat/pangea/models/choreo_record.dart';
import 'package:fluffychat/pangea/models/lemma.dart';
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/models/representation_content_model.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/models/speech_to_text_models.dart';
@ -658,14 +664,145 @@ class PangeaMessageEvent {
}
}
// List<SpanData> get activities =>
//each match is turned into an activity that other students can access
//they're not told the answer but have to find it themselves
//the message has a blank piece which they fill in themselves
List<OneConstructUse> get allConstructUses =>
[...grammarConstructUses, ..._vocabUses];
// replication of logic from message_content.dart
// bool get isHtml =>
// AppConfig.renderHtml && !_event.redacted && _event.isRichMessage;
/// [tokens] is the final list of tokens that were sent
/// if no ga or ta,
/// make wa use for each and return
/// else
/// for each saveable vocab in the final message
/// if vocab is contained in an accepted replacement, make ga use
/// if vocab is contained in ta choice,
/// if selected as choice, corIt
/// if written as customInput, corIt? (account for score in this)
/// for each it step
/// for each continuance
/// if not within the final message, save ignIT/incIT
List<OneConstructUse> get _vocabUses {
final List<OneConstructUse> uses = [];
if (event.roomId == null) return uses;
List<OneConstructUse> lemmasToVocabUses(
List<Lemma> lemmas,
ConstructUseTypeEnum type,
) {
final List<OneConstructUse> uses = [];
for (final lemma in lemmas) {
if (lemma.saveVocab) {
uses.add(
OneConstructUse(
useType: type,
chatId: event.roomId!,
timeStamp: event.originServerTs,
lemma: lemma.text,
form: lemma.form,
msgId: event.eventId,
constructType: ConstructTypeEnum.vocab,
),
);
}
}
return uses;
}
List<OneConstructUse> getVocabUseForToken(PangeaToken token) {
if (originalSent?.choreo == null) {
final bool inUserL2 = originalSent?.langCode == l2Code;
return lemmasToVocabUses(
token.lemmas,
inUserL2 ? ConstructUseTypeEnum.wa : ConstructUseTypeEnum.unk,
);
}
for (final step in originalSent!.choreo!.choreoSteps) {
/// if 1) accepted match 2) token is in the replacement and 3) replacement
/// is in the overall step text, then token was a ga
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted &&
(step.acceptedOrIgnoredMatch!.match.choices?.any(
(r) =>
r.value.contains(token.text.content) &&
step.text.contains(r.value),
) ??
false)) {
return lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.ga);
}
if (step.itStep != null) {
final bool pickedThroughIT = step.itStep!.chosenContinuance?.text
.contains(token.text.content) ??
false;
if (pickedThroughIT) {
return lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.corIt);
//PTODO - check if added via custom input in IT flow
}
}
}
return lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.wa);
}
/// for each token, record whether selected in ga, ta, or wa
if (originalSent?.tokens != null) {
for (final token in originalSent!.tokens!) {
uses.addAll(getVocabUseForToken(token));
}
}
if (originalSent?.choreo == null) return uses;
for (final itStep in originalSent!.choreo!.itSteps) {
for (final continuance in itStep.continuances) {
// this seems to always be false for continuances right now
if (originalSent!.choreo!.finalMessage.contains(continuance.text)) {
continue;
}
if (continuance.wasClicked) {
//PTODO - account for end of flow score
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
uses.addAll(
lemmasToVocabUses(continuance.lemmas, ConstructUseTypeEnum.incIt),
);
}
} else {
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
uses.addAll(
lemmasToVocabUses(continuance.lemmas, ConstructUseTypeEnum.ignIt),
);
}
}
}
}
return uses;
}
List<OneConstructUse> get grammarConstructUses {
final List<OneConstructUse> uses = [];
if (originalSent?.choreo == null || event.roomId == null) return uses;
for (final step in originalSent!.choreo!.choreoSteps) {
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) {
final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ??
step.acceptedOrIgnoredMatch!.match.shortMessage ??
step.acceptedOrIgnoredMatch!.match.type.typeName.name;
uses.add(
OneConstructUse(
useType: ConstructUseTypeEnum.ga,
chatId: event.roomId!,
timeStamp: event.originServerTs,
lemma: name,
form: name,
msgId: event.eventId,
constructType: ConstructTypeEnum.grammar,
id: "${event.eventId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}",
),
);
}
}
return uses;
}
}
class URLFinder {

View file

@ -1,24 +0,0 @@
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
import 'package:matrix/matrix.dart';
import '../constants/pangea_event_types.dart';
class PracticeActivityRecordEvent {
Event event;
PracticeActivityRecordModel? _content;
PracticeActivityRecordEvent({required this.event}) {
if (event.type != PangeaEventTypes.activityRecord) {
throw Exception(
"${event.type} should not be used to make a PracticeActivityRecordEvent",
);
}
}
PracticeActivityRecordModel? get record {
_content ??= event.getPangeaContent<PracticeActivityRecordModel>();
return _content!;
}
}

View file

@ -1,7 +1,7 @@
import 'dart:developer';
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
@ -61,6 +61,8 @@ class PracticeActivityEvent {
)
.toList();
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 => userRecords.isNotEmpty;

View file

@ -0,0 +1,89 @@
import 'dart:developer';
import 'package:fluffychat/pangea/extensions/pangea_event_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/practice_activities.dart/practice_activity_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import '../constants/pangea_event_types.dart';
class PracticeActivityRecordEvent {
Event event;
PracticeActivityRecordModel? _content;
PracticeActivityRecordEvent({required this.event}) {
if (event.type != PangeaEventTypes.activityRecord) {
throw Exception(
"${event.type} should not be used to make a PracticeActivityRecordEvent",
);
}
}
PracticeActivityRecordModel get record {
_content ??= event.getPangeaContent<PracticeActivityRecordModel>();
return _content!;
}
Future<List<OneConstructUse>> uses(Timeline timeline) async {
try {
final String? parent = event.relationshipEventId;
if (parent == null) {
debugger(when: kDebugMode);
ErrorHandler.logError(
m: "PracticeActivityRecordEvent has null event.relationshipEventId",
data: event.toJson(),
);
return [];
}
final Event? practiceEvent =
await timeline.getEventById(event.relationshipEventId!);
if (practiceEvent == null) {
debugger(when: kDebugMode);
ErrorHandler.logError(
m: "PracticeActivityRecordEvent has null practiceActivityEvent with id $parent",
data: event.toJson(),
);
return [];
}
final PracticeActivityEvent practiceActivity = PracticeActivityEvent(
event: practiceEvent,
timeline: timeline,
);
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: record.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,
chatId: event.roomId ?? practiceEvent.roomId ?? timeline.room.id,
msgId: practiceActivity.parentMessageId,
timeStamp: event.originServerTs,
),
);
}
return uses;
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: e, s: s, data: event.toJson());
rethrow;
}
}
}

View file

@ -12,7 +12,11 @@ abstract class AnalyticsModel {
case PangeaEventTypes.summaryAnalytics:
return SummaryAnalyticsModel.formatSummaryContent(recentMsgs);
case PangeaEventTypes.construct:
return ConstructAnalyticsModel.formatConstructsContent(recentMsgs);
final List<OneConstructUse> uses = [];
for (final msg in recentMsgs) {
uses.addAll(msg.allConstructUses);
}
return uses;
}
return [];
}

View file

@ -1,11 +1,9 @@
import 'dart:developer';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import '../../enum/construct_type_enum.dart';
@ -24,7 +22,7 @@ class ConstructAnalyticsModel extends AnalyticsModel {
if (json[_usesKey] is List) {
// This is the new format
uses.addAll(
json[_usesKey]
(json[_usesKey] as List)
.map((use) => OneConstructUse.fromJson(use))
.cast<OneConstructUse>()
.toList(),
@ -39,13 +37,13 @@ class ConstructAnalyticsModel extends AnalyticsModel {
final lemmaUses = useValue[_usesKey];
for (final useData in lemmaUses) {
final use = OneConstructUse(
useType: ConstructUseType.ga,
useType: ConstructUseTypeEnum.ga,
chatId: useData["chatId"],
timeStamp: DateTime.parse(useData["timeStamp"]),
lemma: lemma,
form: useData["form"],
msgId: useData["msgId"],
constructType: ConstructType.grammar,
constructType: ConstructTypeEnum.grammar,
);
uses.add(use);
}
@ -70,122 +68,13 @@ class ConstructAnalyticsModel extends AnalyticsModel {
_usesKey: uses.map((use) => use.toJson()).toList(),
};
}
static List<OneConstructUse> formatConstructsContent(
List<PangeaMessageEvent> recentMsgs,
) {
final List<PangeaMessageEvent> filtered = List.from(recentMsgs);
final List<OneConstructUse> uses = [];
for (final msg in filtered) {
if (msg.originalSent?.choreo == null) continue;
uses.addAll(
msg.originalSent!.choreo!.toGrammarConstructUse(
msg.eventId,
msg.room.id,
msg.originServerTs,
),
);
final List<PangeaToken>? tokens = msg.originalSent?.tokens;
if (tokens == null) continue;
uses.addAll(
msg.originalSent!.choreo!.toVocabUse(
tokens,
msg.room.id,
msg.eventId,
msg.originServerTs,
),
);
}
return uses;
}
}
enum ConstructUseType {
/// produced in chat by user, igc was run, and we've judged it to be a correct use
wa,
/// produced in chat by user, igc was run, and we've judged it to be a incorrect use
/// Note: if the IGC match is ignored, this is not counted as an incorrect use
ga,
/// produced in chat by user and igc was not run
unk,
/// selected correctly in IT flow
corIt,
/// encountered as IT distractor and correctly ignored it
ignIt,
/// encountered as it distractor and selected it
incIt,
/// encountered in igc match and ignored match
ignIGC,
/// selected correctly in IGC flow
corIGC,
/// encountered as distractor in IGC flow and selected it
incIGC,
}
extension on ConstructUseType {
String get string {
switch (this) {
case ConstructUseType.ga:
return 'ga';
case ConstructUseType.wa:
return 'wa';
case ConstructUseType.corIt:
return 'corIt';
case ConstructUseType.incIt:
return 'incIt';
case ConstructUseType.ignIt:
return 'ignIt';
case ConstructUseType.ignIGC:
return 'ignIGC';
case ConstructUseType.corIGC:
return 'corIGC';
case ConstructUseType.incIGC:
return 'incIGC';
case ConstructUseType.unk:
return 'unk';
}
}
IconData get icon {
switch (this) {
case ConstructUseType.ga:
return Icons.check;
case ConstructUseType.wa:
return Icons.thumb_up_sharp;
case ConstructUseType.corIt:
return Icons.check;
case ConstructUseType.incIt:
return Icons.close;
case ConstructUseType.ignIt:
return Icons.close;
case ConstructUseType.ignIGC:
return Icons.close;
case ConstructUseType.corIGC:
return Icons.check;
case ConstructUseType.incIGC:
return Icons.close;
case ConstructUseType.unk:
return Icons.help;
}
}
}
class OneConstructUse {
String? lemma;
ConstructType? constructType;
ConstructTypeEnum? constructType;
String? form;
ConstructUseType useType;
ConstructUseTypeEnum useType;
String chatId;
String? msgId;
DateTime timeStamp;
@ -204,7 +93,7 @@ class OneConstructUse {
factory OneConstructUse.fromJson(Map<String, dynamic> json) {
return OneConstructUse(
useType: ConstructUseType.values
useType: ConstructUseTypeEnum.values
.firstWhere((e) => e.string == json['useType']),
chatId: json['chatId'],
timeStamp: DateTime.parse(json['timeStamp']),
@ -248,7 +137,7 @@ class OneConstructUse {
class ConstructUses {
final List<OneConstructUse> uses;
final ConstructType constructType;
final ConstructTypeEnum constructType;
final String lemma;
ConstructUses({

View file

@ -1,13 +1,8 @@
import 'dart:convert';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import '../constants/choreo_constants.dart';
import '../enum/construct_type_enum.dart';
import 'it_step.dart';
import 'lemma.dart';
/// this class lives within a [PangeaIGCEvent]
/// it always has a [RepresentationEvent] parent
@ -111,135 +106,6 @@ class ChoreoRecord {
openMatches: [],
);
/// [tokens] is the final list of tokens that were sent
/// if no ga or ta,
/// make wa use for each and return
/// else
/// for each saveable vocab in the final message
/// if vocab is contained in an accepted replacement, make ga use
/// if vocab is contained in ta choice,
/// if selected as choice, corIt
/// if written as customInput, corIt? (account for score in this)
/// for each it step
/// for each continuance
/// if not within the final message, save ignIT/incIT
List<OneConstructUse> toVocabUse(
List<PangeaToken> tokens,
String chatId,
String msgId,
DateTime timestamp,
) {
final List<OneConstructUse> uses = [];
final DateTime now = DateTime.now();
List<OneConstructUse> lemmasToVocabUses(
List<Lemma> lemmas,
ConstructUseType type,
) {
final List<OneConstructUse> uses = [];
for (final lemma in lemmas) {
if (lemma.saveVocab) {
uses.add(
OneConstructUse(
useType: type,
chatId: chatId,
timeStamp: timestamp,
lemma: lemma.text,
form: lemma.form,
msgId: msgId,
constructType: ConstructType.vocab,
),
);
}
}
return uses;
}
List<OneConstructUse> getVocabUseForToken(PangeaToken token) {
for (final step in choreoSteps) {
/// if 1) accepted match 2) token is in the replacement and 3) replacement
/// is in the overall step text, then token was a ga
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted &&
(step.acceptedOrIgnoredMatch!.match.choices?.any(
(r) =>
r.value.contains(token.text.content) &&
step.text.contains(r.value),
) ??
false)) {
return lemmasToVocabUses(token.lemmas, ConstructUseType.ga);
}
if (step.itStep != null) {
final bool pickedThroughIT = step.itStep!.chosenContinuance?.text
.contains(token.text.content) ??
false;
if (pickedThroughIT) {
return lemmasToVocabUses(token.lemmas, ConstructUseType.corIt);
//PTODO - check if added via custom input in IT flow
}
}
}
return lemmasToVocabUses(token.lemmas, ConstructUseType.wa);
}
/// for each token, record whether selected in ga, ta, or wa
for (final token in tokens) {
uses.addAll(getVocabUseForToken(token));
}
for (final itStep in itSteps) {
for (final continuance in itStep.continuances) {
// this seems to always be false for continuances right now
if (finalMessage.contains(continuance.text)) {
continue;
}
if (continuance.wasClicked) {
//PTODO - account for end of flow score
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
uses.addAll(
lemmasToVocabUses(continuance.lemmas, ConstructUseType.incIt),
);
}
} else {
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
uses.addAll(
lemmasToVocabUses(continuance.lemmas, ConstructUseType.ignIt),
);
}
}
}
}
return uses;
}
List<OneConstructUse> toGrammarConstructUse(
String msgId,
String chatId,
DateTime timestamp,
) {
final List<OneConstructUse> uses = [];
for (final step in choreoSteps) {
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) {
final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ??
step.acceptedOrIgnoredMatch!.match.shortMessage ??
step.acceptedOrIgnoredMatch!.match.type.typeName.name;
uses.add(
OneConstructUse(
useType: ConstructUseType.ga,
chatId: chatId,
timeStamp: timestamp,
lemma: name,
form: name,
msgId: msgId,
constructType: ConstructType.grammar,
id: "${msgId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}",
),
);
}
}
return uses;
}
List<ITStep> get itSteps =>
choreoSteps.where((e) => e.itStep != null).map((e) => e.itStep!).toList();

View file

@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:developer';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
@ -154,32 +155,37 @@ class VocabTotals {
void addVocabUseBasedOnUseType(List<OneConstructUse> uses) {
for (final use in uses) {
switch (use.useType) {
case ConstructUseType.ga:
case ConstructUseTypeEnum.ga:
ga++;
break;
case ConstructUseType.wa:
case ConstructUseTypeEnum.wa:
wa++;
break;
case ConstructUseType.corIt:
case ConstructUseTypeEnum.corIt:
corIt++;
break;
case ConstructUseType.incIt:
case ConstructUseTypeEnum.incIt:
incIt++;
break;
case ConstructUseType.ignIt:
case ConstructUseTypeEnum.ignIt:
ignIt++;
break;
//TODO - these shouldn't be counted as such
case ConstructUseType.ignIGC:
case ConstructUseTypeEnum.ignIGC:
ignIt++;
break;
case ConstructUseType.corIGC:
case ConstructUseTypeEnum.corIGC:
corIt++;
break;
case ConstructUseType.incIGC:
case ConstructUseTypeEnum.incIGC:
incIt++;
break;
case ConstructUseType.unk:
//TODO if we bring back Headwords then we need to add these
case ConstructUseTypeEnum.corPA:
break;
case ConstructUseTypeEnum.incPA:
break;
case ConstructUseTypeEnum.unk:
break;
}
}

View file

@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart';
class ConstructIdentifier {
final String lemma;
final ConstructType type;
final ConstructTypeEnum type;
ConstructIdentifier({required this.lemma, required this.type});
@ -16,7 +16,7 @@ class ConstructIdentifier {
try {
return ConstructIdentifier(
lemma: json['lemma'] as String,
type: ConstructType.values.firstWhere(
type: ConstructTypeEnum.values.firstWhere(
(e) => e.string == json['type'],
),
);

View file

@ -5,6 +5,8 @@
import 'dart:developer';
import 'dart:typed_data';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
class PracticeActivityRecordModel {
final String? question;
late List<ActivityRecordResponse> responses;
@ -42,18 +44,25 @@ class PracticeActivityRecordModel {
/// get the latest response index according to the response timeStamp
/// sort the responses by timestamp and get the index of the last response
String? get latestResponse {
ActivityRecordResponse? get latestResponse {
if (responses.isEmpty) {
return null;
}
responses.sort((a, b) => a.timestamp.compareTo(b.timestamp));
return responses[responses.length - 1].text;
return responses[responses.length - 1];
}
ConstructUseTypeEnum get useType => latestResponse?.score != null
? (latestResponse!.score > 0
? ConstructUseTypeEnum.corPA
: ConstructUseTypeEnum.incPA)
: ConstructUseTypeEnum.unk;
void addResponse({
String? text,
Uint8List? audioBytes,
Uint8List? imageBytes,
required double score,
}) {
try {
responses.add(
@ -62,6 +71,7 @@ class PracticeActivityRecordModel {
audioBytes: audioBytes,
imageBytes: imageBytes,
timestamp: DateTime.now(),
score: score,
),
);
} catch (e) {
@ -93,11 +103,13 @@ class ActivityRecordResponse {
final Uint8List? audioBytes;
final Uint8List? imageBytes;
final DateTime timestamp;
final double score;
ActivityRecordResponse({
this.text,
this.audioBytes,
this.imageBytes,
required this.score,
required this.timestamp,
});
@ -107,6 +119,10 @@ class ActivityRecordResponse {
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,
);
}
@ -116,6 +132,7 @@ class ActivityRecordResponse {
'audio': audioBytes,
'image': imageBytes,
'timestamp': timestamp.toIso8601String(),
'score': score,
};
}

View file

@ -35,7 +35,7 @@ class BaseAnalyticsView extends StatelessWidget {
);
case BarChartViewSelection.grammar:
return ConstructList(
constructType: ConstructType.grammar,
constructType: ConstructTypeEnum.grammar,
defaultSelected: controller.widget.defaultSelected,
selected: controller.selected,
controller: controller,

View file

@ -19,7 +19,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
class ConstructList extends StatefulWidget {
final ConstructType constructType;
final ConstructTypeEnum constructType;
final AnalyticsSelected defaultSelected;
final AnalyticsSelected? selected;
final BaseAnalyticsController controller;
@ -94,7 +94,7 @@ class ConstructListView extends StatefulWidget {
}
class ConstructListViewState extends State<ConstructListView> {
final ConstructType constructType = ConstructType.grammar;
final ConstructTypeEnum constructType = ConstructTypeEnum.grammar;
final Map<String, Timeline> _timelinesCache = {};
final Map<String, PangeaMessageEvent> _msgEventCache = {};
final List<PangeaMessageEvent> _msgEvents = [];

View file

@ -2,7 +2,7 @@ import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.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/matrix_event_wrappers/practice_acitivity_record_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_ca
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:get_storage/get_storage.dart';
class PracticeActivityContent extends StatefulWidget {
final PracticeActivityEvent practiceEvent;
@ -65,9 +66,9 @@ class MessagePracticeActivityContentState
recordModel = recordEvent!.record;
//Note that only MultipleChoice activities will have this so we probably should move this logic to the MultipleChoiceActivity widget
selectedChoiceIndex = recordModel?.latestResponse != null
selectedChoiceIndex = recordModel?.latestResponse?.text != null
? widget.practiceEvent.practiceActivity.multipleChoice
?.choiceIndex(recordModel!.latestResponse!)
?.choiceIndex(recordModel!.latestResponse!.text!)
: null;
recordSubmittedPreviousSession = true;
@ -80,6 +81,10 @@ class MessagePracticeActivityContentState
setState(() {
selectedChoiceIndex = index;
recordModel!.addResponse(
score: widget.practiceEvent.practiceActivity.multipleChoice!
.isCorrect(index)
? 1
: 0,
text: widget
.practiceEvent.practiceActivity.multipleChoice!.choices[index],
);