Merge pull request #411 from pangeachat/save-practice

Save-practice [WIP]
pull/1384/head
ggurdin 1 year ago committed by GitHub
commit 8096ed5172
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -3111,7 +3111,7 @@
"prettyGood": "Pretty good! Here's what I would have said.",
"letMeThink": "Hmm, let's see how you did!",
"clickMessageTitle": "Need help?",
"clickMessageBody": "Click messages to access definitions, translations, and audio!",
"clickMessageBody": "Click a message for language help! Click and hold to react 😀.",
"understandingMessagesTitle": "Definitions and translations!",
"understandingMessagesBody": "Click underlined words for definitions. Translate with message options (upper right).",
"allDone": "All done!",

@ -4529,7 +4529,7 @@
"definitions": "definiciones",
"subscribedToUnlockTools": "Suscríbase para desbloquear herramientas lingüísticas, como",
"clickMessageTitle": "¿Necesitas ayuda?",
"clickMessageBody": "Haga clic en los mensajes para acceder a las definiciones, traducciones y audio.",
"clickMessageBody": "¡Lame un mensaje para obtener ayuda con el idioma! Haz clic y mantén presionado para reaccionar 😀",
"more": "Más",
"translationTooltip": "Traducir",
"audioTooltip": "Reproducir audio",

@ -16,7 +16,6 @@ import 'package:fluffychat/pages/chat/recording_dialog.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/enum/use_type.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/choreo_record.dart';
@ -586,7 +585,6 @@ class ChatController extends State<ChatPageWithRoom>
PangeaMessageTokens? tokensSent,
PangeaMessageTokens? tokensWritten,
ChoreoRecord? choreo,
UseType? useType,
}) async {
// Pangea#
if (sendController.text.trim().isEmpty) return;
@ -630,7 +628,6 @@ class ChatController extends State<ChatPageWithRoom>
tokensSent: tokensSent,
tokensWritten: tokensWritten,
choreo: choreo,
useType: useType,
)
.then(
(String? msgEventId) async {
@ -644,7 +641,6 @@ class ChatController extends State<ChatPageWithRoom>
GoogleAnalytics.sendMessage(
room.id,
room.classCode,
useType ?? UseType.un,
);
if (msgEventId == null) {

@ -2,7 +2,7 @@ import 'package:animations/animations.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';

@ -470,7 +470,7 @@ class Message extends StatelessWidget {
?.showUseType ??
false) ...[
pangeaMessageEvent!
.useType
.msgUseType
.iconView(
context,
textColor

@ -917,7 +917,7 @@ class ChatListController extends State<ChatList>
if (mounted) {
GoogleAnalytics.analyticsUserUpdate(client.userID);
await pangeaController.subscriptionController.initialize();
await pangeaController.myAnalytics.addEventsListener();
await pangeaController.myAnalytics.initialize();
pangeaController.afterSyncAndFirstLoginInitialization(context);
await pangeaController.inviteBotToExistingSpaces();
await pangeaController.setPangeaPushRules();

@ -5,13 +5,12 @@ import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pangea/choreographer/controllers/alternative_translator.dart';
import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.dart';
import 'package:fluffychat/pangea/choreographer/controllers/message_options.dart';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/enum/assistance_state_enum.dart';
import 'package:fluffychat/pangea/enum/edit_type.dart';
import 'package:fluffychat/pangea/models/it_step.dart';
import 'package:fluffychat/pangea/models/language_detection_model.dart';
import 'package:fluffychat/pangea/models/representation_content_model.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
@ -25,7 +24,6 @@ import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../../../widgets/matrix.dart';
import '../../enum/use_type.dart';
import '../../models/choreo_record.dart';
import '../../models/language_model.dart';
import '../../models/pangea_match_model.dart';
@ -95,63 +93,59 @@ class Choreographer {
}
Future<void> _sendWithIGC(BuildContext context) async {
if (igc.canSendMessage) {
final PangeaRepresentation? originalWritten =
choreoRecord.includedIT && itController.sourceText != null
? PangeaRepresentation(
langCode: l1LangCode ?? LanguageKeys.unknownLanguage,
text: itController.sourceText!,
originalWritten: true,
originalSent: false,
)
: null;
final PangeaRepresentation originalSent = PangeaRepresentation(
langCode: langCodeOfCurrentText ?? LanguageKeys.unknownLanguage,
text: currentText,
originalSent: true,
originalWritten: originalWritten == null,
);
final ChoreoRecord? applicableChoreo =
isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null;
// if the message has not been processed to determine its language
// then run it through the language detection endpoint. If the detection
// confidence is high enough, use that language code as the message's language
// to save that pangea representation
if (applicableChoreo == null) {
final resp = await pangeaController.languageDetection.detectLanguage(
currentText,
pangeaController.languageController.userL2?.langCode,
pangeaController.languageController.userL1?.langCode,
);
final LanguageDetection? bestDetection = resp.bestDetection();
if (bestDetection != null) {
originalSent.langCode = bestDetection.langCode;
}
}
final UseType useType = useTypeCalculator(applicableChoreo);
debugPrint("use type in choreographer $useType");
chatController.send(
// PTODO - turn this back on in conjunction with saving tokens
// we need to save those tokens as well, in order for exchanges to work
// properly. in an exchange, the other user will want
// originalWritten: originalWritten,
originalSent: originalSent,
tokensSent: igc.igcTextData?.tokens != null
? PangeaMessageTokens(tokens: igc.igcTextData!.tokens)
: null,
//TODO - save originalwritten tokens
choreo: applicableChoreo,
useType: useType,
);
clear();
} else {
if (!igc.canSendMessage) {
igc.showFirstMatch(context);
return;
}
final PangeaRepresentation? originalWritten =
choreoRecord.includedIT && itController.sourceText != null
? PangeaRepresentation(
langCode: l1LangCode ?? LanguageKeys.unknownLanguage,
text: itController.sourceText!,
originalWritten: true,
originalSent: false,
)
: null;
// TODO - why does both it and igc need to be enabled for choreo to be applicable?
// final ChoreoRecord? applicableChoreo =
// isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null;
// if tokens or language detection are not available, we should get them
// notes
// 1) we probably need to move this to after we clear the input field
// or the user could experience some lag here.
// 2) that this call is being made after we've determined if we have an applicable choreo in order to
// say whether correction was run on the message. we may eventually want
// to edit the useType after
if (igc.igcTextData?.tokens == null ||
igc.igcTextData?.detectedLanguage == null) {
await igc.getIGCTextData(onlyTokensAndLanguageDetection: true);
}
final PangeaRepresentation originalSent = PangeaRepresentation(
langCode:
igc.igcTextData?.detectedLanguage ?? LanguageKeys.unknownLanguage,
text: currentText,
originalSent: true,
originalWritten: originalWritten == null,
);
final PangeaMessageTokens? tokensSent = igc.igcTextData?.tokens != null
? PangeaMessageTokens(tokens: igc.igcTextData!.tokens)
: null;
chatController.send(
// originalWritten: originalWritten,
originalSent: originalSent,
tokensSent: tokensSent,
//TODO - save originalwritten tokens
// choreo: applicableChoreo,
choreo: choreoRecord,
);
clear();
}
_resetDebounceTimer() {
@ -167,7 +161,7 @@ class Choreographer {
}
choreoMode = ChoreoMode.it;
itController.initializeIT(
ITStartData(_textController.text, igc.detectedLangCode),
ITStartData(_textController.text, igc.igcTextData?.detectedLanguage),
);
itMatch.status = PangeaMatchStatus.accepted;
@ -180,6 +174,7 @@ class Choreographer {
_textController.setSystemText("", EditType.itStart);
}
/// Handles any changes to the text input
_onChangeListener() {
if (_noChange) {
return;
@ -188,21 +183,26 @@ class Choreographer {
if ([
EditType.igc,
].contains(_textController.editType)) {
// this may be unnecessary now that tokens are not used
// to allow click of words in the input field and we're getting this at the end
// TODO - turn it off and tested that this is fine
igc.justGetTokensAndAddThemToIGCTextData();
// we set editType to keyboard here because that is the default for it
// and we want to make sure that the next change is treated as a keyboard change
// unless the system explicity sets it to something else. this
textController.editType = EditType.keyboard;
return;
}
// not sure if this is necessary now
MatrixState.pAnyState.closeOverlay();
if (errorService.isError) {
return;
}
// if (igc.igcTextData != null) {
igc.clear();
// setState();
// }
_resetDebounceTimer();
@ -212,7 +212,9 @@ class Choreographer {
() => getLanguageHelp(),
);
} else {
getLanguageHelp(ChoreoMode.it == choreoMode);
getLanguageHelp(
onlyTokensAndLanguageDetection: ChoreoMode.it == choreoMode,
);
}
//Note: we don't set the keyboard type on each keyboard stroke so this is how we default to
@ -221,10 +223,14 @@ class Choreographer {
textController.editType = EditType.keyboard;
}
Future<void> getLanguageHelp([
bool tokensOnly = false,
/// Fetches the language help for the current text, including grammar correction, language detection,
/// tokens, and translations. Includes logic to exit the flow if the user is not subscribed, if the tools are not enabled, or
/// or if autoIGC is not enabled and the user has not manually requested it.
/// [onlyTokensAndLanguageDetection] will
Future<void> getLanguageHelp({
bool onlyTokensAndLanguageDetection = false,
bool manual = false,
]) async {
}) async {
try {
if (errorService.isError) return;
final CanSendStatus canSendStatus =
@ -239,13 +245,15 @@ class Choreographer {
startLoading();
if (choreoMode == ChoreoMode.it &&
itController.isTranslationDone &&
!tokensOnly) {
!onlyTokensAndLanguageDetection) {
// debugger(when: kDebugMode);
}
await (choreoMode == ChoreoMode.it && !itController.isTranslationDone
? itController.getTranslationData(_useCustomInput)
: igc.getIGCTextData(tokensOnly: tokensOnly));
: igc.getIGCTextData(
onlyTokensAndLanguageDetection: onlyTokensAndLanguageDetection,
));
} catch (err, stack) {
ErrorHandler.logError(e: err, s: stack);
} finally {
@ -482,14 +490,6 @@ class Choreographer {
bool get editTypeIsKeyboard => EditType.keyboard == _textController.editType;
String? get langCodeOfCurrentText {
if (igc.detectedLangCode != null) return igc.detectedLangCode!;
if (itController.isOpen) return l2LangCode!;
return null;
}
setState() {
if (!stateListener.isClosed) {
stateListener.add(0);
@ -523,9 +523,11 @@ class Choreographer {
chatController.room,
);
bool get itAutoPlayEnabled => pangeaController.pStoreService.read(
bool get itAutoPlayEnabled =>
pangeaController.pStoreService.read(
MatrixProfile.itAutoPlay.title,
) ?? false;
) ??
false;
bool get definitionsEnabled =>
pangeaController.permissionsController.isToolEnabled(

@ -3,18 +3,17 @@ import 'dart:developer';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
import 'package:fluffychat/pangea/controllers/span_data_controller.dart';
import 'package:fluffychat/pangea/choreographer/controllers/span_data_controller.dart';
import 'package:fluffychat/pangea/models/igc_text_data_model.dart';
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/repo/igc_repo.dart';
import 'package:fluffychat/pangea/repo/tokens_repo.dart';
import 'package:fluffychat/pangea/widgets/igc/span_card.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../../models/language_detection_model.dart';
import '../../models/span_card_model.dart';
import '../../repo/tokens_repo.dart';
import '../../utils/error_handler.dart';
import '../../utils/overlay.dart';
@ -29,59 +28,42 @@ class IgcController {
spanDataController = SpanDataController(choreographer);
}
Future<void> getIGCTextData({required bool tokensOnly}) async {
Future<void> getIGCTextData({
required bool onlyTokensAndLanguageDetection,
}) async {
try {
if (choreographer.currentText.isEmpty) return clear();
// the error spans are going to be reloaded, so clear the cache
spanDataController.clearCache();
debugPrint('getIGCTextData called with ${choreographer.currentText}');
debugPrint('getIGCTextData called with tokensOnly = $tokensOnly');
debugPrint(
'getIGCTextData called with tokensOnly = $onlyTokensAndLanguageDetection',
);
final IGCRequestBody reqBody = IGCRequestBody(
fullText: choreographer.currentText,
userL1: choreographer.l1LangCode!,
userL2: choreographer.l2LangCode!,
enableIGC: choreographer.igcEnabled && !tokensOnly,
enableIT: choreographer.itEnabled && !tokensOnly,
tokensOnly: tokensOnly,
enableIGC: choreographer.igcEnabled && !onlyTokensAndLanguageDetection,
enableIT: choreographer.itEnabled && !onlyTokensAndLanguageDetection,
);
final IGCTextData igcTextDataResponse = await IgcRepo.getIGC(
await choreographer.accessToken,
igcRequest: reqBody,
);
// temp fix
igcTextDataResponse.originalInput = reqBody.fullText;
//this will happen when the user changes the input while igc is fetching results
// this will happen when the user changes the input while igc is fetching results
if (igcTextDataResponse.originalInput != choreographer.currentText) {
// final current = choreographer.currentText;
// final igctext = igcTextDataResponse.originalInput;
// Sentry.addBreadcrumb(
// Breadcrumb(message: "igc return input does not match current text"),
// );
// debugger(when: kDebugMode);
return;
}
//TO-DO: in api call, specify turning off IT and/or grammar checking
if (!choreographer.igcEnabled) {
igcTextDataResponse.matches = igcTextDataResponse.matches
.where((match) => !match.isGrammarMatch)
.toList();
}
if (!choreographer.itEnabled) {
igcTextDataResponse.matches = igcTextDataResponse.matches
.where((match) => !match.isOutOfTargetMatch)
.toList();
}
if (!choreographer.itEnabled && !choreographer.igcEnabled) {
igcTextDataResponse.matches = [];
}
igcTextData = igcTextDataResponse;
// TODO - for each new match,
// check if existing igcTextData has one and only one match with the same error text and correction
// if so, keep the original match and discard the new one
// if not, add the new match to the existing igcTextData
// After fetching igc data, pre-call span details for each match optimistically.
// This will make the loading of span details faster for the user
if (igcTextData?.matches.isNotEmpty ?? false) {
@ -170,11 +152,9 @@ class IgcController {
const int firstMatchIndex = 0;
final PangeaMatch match = igcTextData!.matches[firstMatchIndex];
if (
match.isITStart &&
if (match.isITStart &&
choreographer.itAutoPlayEnabled &&
igcTextData != null
) {
igcTextData != null) {
choreographer.onITStart(igcTextData!.matches[firstMatchIndex]);
return;
}
@ -215,14 +195,6 @@ class IgcController {
return true;
}
String? get detectedLangCode {
if (!hasRelevantIGCTextData) return null;
final LanguageDetection first = igcTextData!.detections.first;
return first.langCode;
}
clear() {
igcTextData = null;
spanDataController.clearCache();

@ -72,7 +72,6 @@ class ITController {
/// if IGC isn't positive that text is full L1 then translate to L1
Future<void> _setSourceText() async {
// try {
if (_itStartData == null || _itStartData!.text.isEmpty) {
Sentry.addBreadcrumb(
Breadcrumb(
@ -97,21 +96,12 @@ class ITController {
request: FullTextTranslationRequestModel(
text: _itStartData!.text,
tgtLang: choreographer.l1LangCode!,
srcLang: choreographer.l2LangCode,
srcLang: _itStartData!.langCode,
userL1: choreographer.l1LangCode!,
userL2: choreographer.l2LangCode!,
),
);
sourceText = res.bestTranslation;
// } catch (err, stack) {
// debugger(when: kDebugMode);
// if (_itStartData?.text.isNotEmpty ?? false) {
// ErrorHandler.logError(e: err, s: stack);
// sourceText = _itStartData!.text;
// } else {
// rethrow;
// }
// }
}
// used 1) at very beginning (with custom input = null)
@ -167,7 +157,7 @@ class ITController {
if (isTranslationDone) {
choreographer.altTranslator.setTranslationFeedback();
choreographer.getLanguageHelp(true);
choreographer.getLanguageHelp(onlyTokensAndLanguageDetection: true);
} else {
getNextTranslationData();
}
@ -218,7 +208,6 @@ class ITController {
Future<void> onEditSourceTextSubmit(String newSourceText) async {
try {
_isOpen = true;
_isEditingSourceText = false;
_itStartData = ITStartData(newSourceText, choreographer.l1LangCode);
@ -230,7 +219,6 @@ class ITController {
_setSourceText();
getTranslationData(false);
} catch (err, stack) {
debugger(when: kDebugMode);
if (err is! http.Response) {
@ -332,9 +320,6 @@ class ITController {
bool get isLoading => choreographer.isFetching;
bool get correctChoicesSelected =>
completedITSteps.every((ITStep step) => step.isCorrect);
String latestChoiceFeedback(BuildContext context) =>
completedITSteps.isNotEmpty
? completedITSteps.last.choiceFeedback(context)

@ -1,7 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';

@ -91,8 +91,8 @@ class StartIGCButtonState extends State<StartIGCButton>
if (assistanceState != AssistanceState.fetching) {
widget.controller.choreographer
.getLanguageHelp(
false,
true,
onlyTokensAndLanguageDetection: false,
manual: true,
)
.then((_) {
if (widget.controller.choreographer.igc.igcTextData != null &&

@ -5,7 +5,7 @@ class Environment {
DateTime.utc(2023, 1, 25).isBefore(DateTime.now());
static String get fileName {
return ".env";
return ".local_choreo.env";
}
static bool get isStaging => synapsURL.contains("staging");

@ -1,4 +0,0 @@
class PrefKey {
static const lastFetched = 'LAST_FETCHED';
static const flags = 'flags';
}

@ -0,0 +1,24 @@
import 'package:fluffychat/pangea/models/language_detection_model.dart';
class LanguageKeys {
static const unknownLanguage = "unk";
static const mixedLanguage = "mixed";
static const defaultLanguage = "en";
static const multiLanguage = "multi";
}
class LanguageLevelType {
static List<int> get allInts => [0, 1, 2, 3, 4, 5, 6];
}
class PrefKey {
static const lastFetched = 'p_lang_lastfetched';
static const flags = 'p_lang_flag';
}
final LanguageDetection unknownLanguageDetection = LanguageDetection(
langCode: LanguageKeys.unknownLanguage,
confidence: 0.5,
);
const double languageDetectionConfidenceThreshold = 0.95;

@ -1,6 +0,0 @@
class LanguageKeys {
static const unknownLanguage = "unk";
static const mixedLanguage = "mixed";
static const defaultLanguage = "en";
static const multiLanguage = "multi";
}

@ -1,3 +0,0 @@
class LanguageLevelType {
static List<int> get allInts => [0, 1, 2, 3, 4, 5, 6];
}

@ -1,4 +0,0 @@
class PrefKey {
static const lastFetched = 'p_lang_lastfetched';
static const flags = 'p_lang_flag';
}

@ -66,7 +66,6 @@ class ModelKey {
static const String tokensSent = "tokens_sent";
static const String tokensWritten = "tokens_written";
static const String choreoRecord = "choreo_record";
static const String useType = "use_type";
static const String baseDefinition = "base_definition";
static const String targetDefinition = "target_definition";

@ -1,6 +1,6 @@
import 'dart:developer';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/models/language_model.dart';

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:fluffychat/pangea/config/environment.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/models/language_detection_model.dart';
import 'package:fluffychat/pangea/network/urls.dart';
@ -75,19 +76,21 @@ class LanguageDetectionResponse {
};
}
LanguageDetection? get _bestDetection {
/// Return the highest confidence detection.
/// If there are no detections, the unknown language detection is returned.
LanguageDetection get highestConfidenceDetection {
detections.sort((a, b) => b.confidence.compareTo(a.confidence));
return detections.isNotEmpty ? detections.first : null;
return detections.firstOrNull ?? unknownLanguageDetection;
}
final double _confidenceThreshold = 0.95;
LanguageDetection? bestDetection({double? threshold}) {
threshold ??= _confidenceThreshold;
return (_bestDetection?.confidence ?? 0) >= _confidenceThreshold
? _bestDetection!
: null;
}
/// Returns the highest validated detection based on the confidence threshold.
/// If the highest confidence detection is below the threshold, the unknown language
/// detection is returned.
LanguageDetection highestValidatedDetection({double? threshold}) =>
highestConfidenceDetection.confidence >=
(threshold ?? languageDetectionConfidenceThreshold)
? highestConfidenceDetection
: unknownLanguageDetection;
}
class _LanguageDetectionCacheItem {

@ -1,13 +1,12 @@
import 'dart:async';
import 'dart:developer';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/models/language_model.dart';
import 'package:fluffychat/pangea/repo/language_repo.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import '../constants/language_list_keys.dart';
import '../utils/shared_prefs.dart';
class PangeaLanguage {

@ -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({

@ -1,15 +1,12 @@
import 'dart:async';
import 'dart:developer';
import 'package:fluffychat/pangea/constants/language_keys.dart';
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/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_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_event.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
@ -18,11 +15,18 @@ import 'package:matrix/matrix.dart';
import '../extensions/client_extension/client_extension.dart';
import '../extensions/pangea_room_extension/pangea_room_extension.dart';
// controls the sending of analytics events
class MyAnalyticsController extends BaseController {
/// handles the processing of analytics for
/// 1) messages sent by the user and
/// 2) constructs used by the user, both in sending messages and doing practice activities
class MyAnalyticsController {
late PangeaController _pangeaController;
Timer? _updateTimer;
/// the max number of messages that will be cached before
/// an automatic update is triggered
final int _maxMessagesCached = 10;
/// the number of minutes before an automatic update is triggered
final int _minutesBeforeUpdate = 5;
/// the time since the last update that will trigger an automatic update
@ -33,41 +37,50 @@ class MyAnalyticsController extends BaseController {
}
/// adds the listener that handles when to run automatic updates
/// to analytics - either after a certain number of messages sent/
/// to analytics - either after a certain number of messages sent
/// received or after a certain amount of time [_timeSinceUpdate] without an update
Future<void> addEventsListener() async {
final Client client = _pangeaController.matrixState.client;
Future<void> initialize() async {
final lastUpdated = await _refreshAnalyticsIfOutdated();
// listen for new messages and updateAnalytics timer
// we are doing this in an attempt to update analytics when activitiy is low
// both in messages sent by this client and other clients that you're connected with
// doesn't account for messages sent by other clients that you're not connected with
_client.onSync.stream
.where((SyncUpdate update) => update.rooms?.join != null)
.listen((update) {
updateAnalyticsTimer(update, lastUpdated);
});
}
// if analytics haven't been updated in the last day, update them
/// If analytics haven't been updated in the last day, update them
Future<DateTime?> _refreshAnalyticsIfOutdated() async {
DateTime? lastUpdated = await _pangeaController.analytics
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
final DateTime yesterday = DateTime.now().subtract(_timeSinceUpdate);
if (lastUpdated?.isBefore(yesterday) ?? true) {
debugPrint("analytics out-of-date, updating");
await updateAnalytics();
lastUpdated = await _pangeaController.analytics
.myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics);
}
client.onSync.stream
.where((SyncUpdate update) => update.rooms?.join != null)
.listen((update) {
updateAnalyticsTimer(update, lastUpdated);
});
return lastUpdated;
}
/// given an update from sync stream, check if the update contains
Client get _client => _pangeaController.matrixState.client;
/// Given an update from sync stream, check if the update contains
/// messages for which analytics will be saved. If so, reset the timer
/// and add the event ID to the cache of un-added event IDs
void updateAnalyticsTimer(SyncUpdate update, DateTime? lastUpdated) {
for (final entry in update.rooms!.join!.entries) {
final Room room =
_pangeaController.matrixState.client.getRoomById(entry.key)!;
final Room room = _client.getRoomById(entry.key)!;
// get the new events in this sync that are messages
final List<Event>? events = entry.value.timeline?.events
?.map((event) => Event.fromMatrixEvent(event, room))
.where((event) => eventHasAnalytics(event, lastUpdated))
.where((event) => hasUserAnalyticsToCache(event, lastUpdated))
.toList();
// add their event IDs to the cache of un-added event IDs
@ -87,8 +100,9 @@ class MyAnalyticsController extends BaseController {
}
// checks if event from sync update is a message that should have analytics
bool eventHasAnalytics(Event event, DateTime? lastUpdated) {
return (lastUpdated == null || event.originServerTs.isAfter(lastUpdated)) &&
bool hasUserAnalyticsToCache(Event event, DateTime? lastUpdated) {
return event.senderId == _client.userID &&
(lastUpdated == null || event.originServerTs.isAfter(lastUpdated)) &&
event.type == EventTypes.Message &&
event.messageType == MessageTypes.Text &&
!(event.eventId.contains("web") &&
@ -176,192 +190,135 @@ class MyAnalyticsController extends BaseController {
}
}
// top level analytics sending function. Send analytics
// for each type of analytics event
// to each of the applicable analytics rooms
String? get userL2 => _pangeaController.languageController.activeL2Code();
/// 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 the user's l2 is not sent, don't send analytics
final String? userL2 = _pangeaController.languageController.activeL2Code();
if (userL2 == null) {
// if missing important info, don't send analytics
if (userL2 == null || _client.userID == null) {
debugger(when: kDebugMode);
return;
}
// fetch a list of all the chats that the user is studying
// and a list of all the spaces in which the user is studying
await setStudentChats();
await setStudentSpaces();
// 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();
// analytics room for the user and current target language
final Room analyticsRoom = await _client.getMyAnalyticsRoom(userL2!);
/// 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;
}
// for each chat the user is studying in, get all the messages
// since the least recent update analytics update, and sort them
// by their langCodes
final Map<String, List<PangeaMessageEvent>> langCodeToMsgs =
await getLangCodesToMsgs(
userL2,
l2AnalyticsLastUpdated,
// get the last time analytics were updated for this room
final DateTime? l2AnalyticsLastUpdated =
await analyticsRoom.analyticsLastUpdated(
PangeaEventTypes.summaryAnalytics,
_client.userID!,
);
final List<String> langCodes = langCodeToMsgs.keys.toList();
for (final String langCode in langCodes) {
// for each of the langs that the user has sent message in, get
// the corresponding analytics room (or create it)
final Room analyticsRoom = await _pangeaController.matrixState.client
.getMyAnalyticsRoom(langCode);
// 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;
// get the corresponding list of recent messages for this langCode
final List<PangeaMessageEvent> recentMsgs =
langCodeToMsgs[langCode] ?? [];
// finally, send the analytics events to the analytics room
await sendAnalyticsEvents(
analyticsRoom,
recentMsgs,
lastUpdated,
// all chats in which user is a student
final List<Room> chats = _client.rooms
.where((room) => !room.isSpace && !room.isAnalyticsRoom)
.toList();
// 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) {
recentMsgFutures.add(
chat.getEventsBySender(
type: EventTypes.Message,
sender: _client.userID!,
since: l2AnalyticsLastUpdated,
),
);
recentActivityFutures.add(
chat.getEventsBySender(
type: PangeaEventTypes.activityRecord,
sender: _client.userID!,
since: l2AnalyticsLastUpdated,
),
);
}
}
Future<Map<String, List<PangeaMessageEvent>>> getLangCodesToMsgs(
String userL2,
DateTime? since,
) async {
// get a map of langCodes to messages for each chat the user is studying in
final Map<String, List<PangeaMessageEvent>> langCodeToMsgs = {};
for (final Room chat in _studentChats) {
List<PangeaMessageEvent>? recentMsgs;
try {
recentMsgs = await chat.myMessageEventsInChat(
since: since,
);
} catch (err) {
debugPrint("failed to fetch messages for chat ${chat.id}");
continue;
}
// sort those messages by their langCode
// langCode is hopefully based on the original sent rep, but if that
// is null or unk, it will be based on the user's current l2
for (final msg in recentMsgs) {
final String msgLangCode = (msg.originalSent?.langCode != null &&
msg.originalSent?.langCode != LanguageKeys.unknownLanguage)
? msg.originalSent!.langCode
: userL2;
langCodeToMsgs[msgLangCode] ??= [];
langCodeToMsgs[msgLangCode]!.add(msg);
}
final List<List<Event>> recentMsgs =
(await Future.wait(recentMsgFutures)).toList();
final List<PracticeActivityRecordEvent> recentActivityRecords =
(await Future.wait(recentActivityFutures))
.expand((e) => e)
.map((event) => PracticeActivityRecordEvent(event: event))
.toList();
// get the timelines for each chat
final List<Future<Timeline>> timelineFutures = [];
for (final chat in chats) {
timelineFutures.add(chat.getTimeline());
}
return langCodeToMsgs;
}
Future<void> sendAnalyticsEvents(
Room analyticsRoom,
List<PangeaMessageEvent> recentMsgs,
DateTime? lastUpdated,
) async {
// remove messages that were sent before the last update
if (recentMsgs.isEmpty) return;
if (lastUpdated != null) {
recentMsgs.removeWhere(
(msg) => msg.event.originServerTs.isBefore(lastUpdated),
final List<Timeline> timelines = await Future.wait(timelineFutures);
final Map<String, Timeline> timelineMap =
Map.fromIterables(chats.map((e) => e.id), timelines);
//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(),
);
}
// format the analytics data
final List<RecentMessageRecord> summaryContent =
SummaryAnalyticsModel.formatSummaryContent(recentMsgs);
final List<OneConstructUse> constructContent =
ConstructAnalyticsModel.formatConstructsContent(recentMsgs);
final List<PangeaMessageEvent> allRecentMessages =
recentPangeaMessageEvents.expand((e) => e).toList();
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 || lastUpdated == null) {
await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent(
analyticsRoom,
if (summaryContent.isNotEmpty || l2AnalyticsLastUpdated == null) {
await analyticsRoom.sendSummaryAnalyticsEvent(
summaryContent,
);
}
if (constructContent.isNotEmpty) {
await ConstructAnalyticsEvent.sendConstructsEvent(
analyticsRoom,
constructContent,
);
// get constructs for messages
final List<OneConstructUse> recentConstructUses = [];
for (final PangeaMessageEvent message in allRecentMessages) {
recentConstructUses.addAll(message.allConstructUses);
}
}
List<Room> _studentChats = [];
Future<void> setStudentChats() async {
final List<String> teacherRoomIds =
await _pangeaController.matrixState.client.teacherRoomIds;
_studentChats = _pangeaController.matrixState.client.rooms
.where(
(r) =>
!r.isSpace &&
!r.isAnalyticsRoom &&
!teacherRoomIds.contains(r.id),
)
.toList();
setState(data: _studentChats);
}
List<Room> get studentChats {
try {
if (_studentChats.isNotEmpty) return _studentChats;
setStudentChats();
return _studentChats;
} catch (err) {
debugger(when: kDebugMode);
return [];
// get constructs for practice activities
final List<Future<List<OneConstructUse>>> constructFutures = [];
for (final PracticeActivityRecordEvent activity in recentActivityRecords) {
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;
}
constructFutures.add(activity.uses(timeline));
}
}
List<Room> _studentSpaces = [];
Future<void> setStudentSpaces() async {
_studentSpaces =
await _pangeaController.matrixState.client.spacesImStudyingIn;
}
List<Room> get studentSpaces {
try {
if (_studentSpaces.isNotEmpty) return _studentSpaces;
setStudentSpaces();
return _studentSpaces;
} catch (err) {
debugger(when: kDebugMode);
return [];
final List<List<OneConstructUse>> constructLists =
await Future.wait(constructFutures);
recentConstructUses.addAll(constructLists.expand((e) => e));
//TODO - confirm that this is the correct construct content
// debugger(
// when: kDebugMode,
// );
// ; debugger(
// when: kDebugMode &&
// (allRecentMessages.isNotEmpty || recentActivityRecords.isNotEmpty),
// );
if (recentConstructUses.isNotEmpty) {
await analyticsRoom.sendConstructsEvent(
recentConstructUses,
);
}
}
}

@ -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,

@ -1,7 +1,7 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/controllers/base_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';

@ -1,7 +1,7 @@
import 'package:collection/collection.dart';
import 'package:http/http.dart' as http;
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/repo/word_repo.dart';
import '../models/word_data_model.dart';
import 'base_controller.dart';

@ -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;
}
}
}

@ -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;
}
}
}

@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../models/choreo_record.dart';
import '../utils/bot_style.dart';
enum UseType { wa, ta, ga, un }
@ -93,17 +91,3 @@ extension UseTypeMethods on UseType {
}
}
}
UseType useTypeCalculator(
ChoreoRecord? choreoRecord,
) {
if (choreoRecord == null) {
return UseType.un;
} else if (choreoRecord.includedIT) {
return UseType.ta;
} else if (choreoRecord.hasAcceptedMatches) {
return UseType.ga;
} else {
return UseType.wa;
}
}

@ -51,7 +51,9 @@ extension PangeaClient on Client {
Future<List<Room>> get spacesImTeaching async => await _spacesImTeaching;
Future<List<Room>> get spacesImStudyingIn async => await _spacesImStudyingIn;
Future<List<Room>> get chatsImAStudentIn async => await _chatsImAStudentIn;
Future<List<Room>> get spaceImAStudentIn async => await _spacesImStudyingIn;
List<Room> get spacesImIn => _spacesImIn;

@ -19,6 +19,18 @@ extension SpaceClientExtension on Client {
return spaces;
}
Future<List<Room>> get _chatsImAStudentIn async {
final List<String> nowteacherRoomIds = await teacherRoomIds;
return rooms
.where(
(r) =>
!r.isSpace &&
!r.isAnalyticsRoom &&
!nowteacherRoomIds.contains(r.id),
)
.toList();
}
Future<List<Room>> get _spacesImStudyingIn async {
final List<Room> joinedSpaces = rooms
.where(

@ -229,7 +229,6 @@ extension EventsRoomExtension on Room {
PangeaMessageTokens? tokensSent,
PangeaMessageTokens? tokensWritten,
ChoreoRecord? choreo,
UseType? useType,
}) {
// if (parseCommands) {
// return client.parseAndRunCommand(this, message,
@ -247,7 +246,6 @@ extension EventsRoomExtension on Room {
ModelKey.originalWritten: originalWritten?.toJson(),
ModelKey.tokensSent: tokensSent?.toJson(),
ModelKey.tokensWritten: tokensWritten?.toJson(),
ModelKey.useType: useType?.string,
};
if (parseMarkdown) {
final html = markdown(
@ -347,7 +345,7 @@ extension EventsRoomExtension on Room {
RecentMessageRecord(
eventId: event.eventId,
chatId: id,
useType: pMsgEvent.useType,
useType: pMsgEvent.msgUseType,
time: event.originServerTs,
),
);
@ -426,26 +424,6 @@ extension EventsRoomExtension on Room {
// }
// }
Future<List<PangeaMessageEvent>> myMessageEventsInChat({
DateTime? since,
}) async {
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();
}
// fetch event of a certain type by a certain sender
// since a certain time or up to a certain amount
Future<List<Event>> getEventsBySender({

@ -4,13 +4,14 @@ import 'dart:developer';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/class_default_values.dart';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
import 'package:fluffychat/pangea/models/bot_options_model.dart';
@ -33,7 +34,6 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import '../../../config/app_config.dart';
import '../../constants/pangea_event_types.dart';
import '../../enum/use_type.dart';
import '../../models/choreo_record.dart';
import '../../models/representation_content_model.dart';
import '../client_extension/client_extension.dart';
@ -180,7 +180,6 @@ extension PangeaRoom on Room {
PangeaMessageTokens? tokensSent,
PangeaMessageTokens? tokensWritten,
ChoreoRecord? choreo,
UseType? useType,
}) =>
_pangeaSendTextEvent(
message,
@ -197,7 +196,6 @@ extension PangeaRoom on Room {
tokensSent: tokensSent,
tokensWritten: tokensWritten,
choreo: choreo,
useType: useType,
);
Future<String> updateStateEvent(Event stateEvent) =>

@ -99,7 +99,7 @@ extension AnalyticsRoomExtension on Room {
return;
}
for (final Room space in (await client.spacesImStudyingIn)) {
for (final Room space in (await client.spaceImAStudentIn)) {
if (space.spaceChildren.any((sc) => sc.roomId == id)) continue;
await space.addAnalyticsRoomToSpace(this);
}
@ -175,7 +175,7 @@ extension AnalyticsRoomExtension on Room {
return;
}
for (final Room space in (await client.spacesImStudyingIn)) {
for (final Room space in (await client.spaceImAStudentIn)) {
await space.inviteSpaceTeachersToAnalyticsRoom(this);
}
}
@ -194,7 +194,7 @@ extension AnalyticsRoomExtension on Room {
final List<Event> events = await getEventsBySender(
type: type,
sender: userId,
count: 1,
count: 10,
);
if (events.isEmpty) return null;
final Event event = events.first;
@ -249,4 +249,31 @@ extension AnalyticsRoomExtension on Room {
return creationContent?.tryGet<String>(ModelKey.langCode) == langCode ||
creationContent?.tryGet<String>(ModelKey.oldLangCode) == langCode;
}
Future<String?> sendSummaryAnalyticsEvent(
List<RecentMessageRecord> records,
) async {
final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel(
messages: records,
);
final String? eventId = await sendEvent(
analyticsModel.toJson(),
type: PangeaEventTypes.summaryAnalytics,
);
return eventId;
}
Future<String?> sendConstructsEvent(
List<OneConstructUse> uses,
) async {
final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel(
uses: uses,
);
final String? eventId = await sendEvent(
constructsModel.toJson(),
type: PangeaEventTypes.construct,
);
return eventId;
}
}

@ -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';
@ -22,7 +28,7 @@ import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../../widgets/matrix.dart';
import '../constants/language_keys.dart';
import '../constants/language_constants.dart';
import '../constants/pangea_event_types.dart';
import '../enum/use_type.dart';
import '../utils/error_handler.dart';
@ -31,7 +37,6 @@ class PangeaMessageEvent {
late Event _event;
final Timeline timeline;
final bool ownMessage;
bool _isValidPangeaMessageEvent = true;
PangeaMessageEvent({
required Event event,
@ -39,7 +44,7 @@ class PangeaMessageEvent {
required this.ownMessage,
}) {
if (event.type != EventTypes.Message) {
_isValidPangeaMessageEvent = false;
debugger(when: kDebugMode);
ErrorHandler.logError(
m: "${event.type} should not be used to make a PangeaMessageEvent",
);
@ -542,7 +547,18 @@ class PangeaMessageEvent {
originalWritten: false,
);
UseType get useType => useTypeCalculator(originalSent?.choreo);
UseType get msgUseType {
final ChoreoRecord? choreoRecord = originalSent?.choreo;
if (choreoRecord == null) {
return UseType.un;
} else if (choreoRecord.includedIT) {
return UseType.ta;
} else if (choreoRecord.hasAcceptedMatches) {
return UseType.ga;
} else {
return UseType.wa;
}
}
bool get showUseType =>
!ownMessage &&
@ -651,21 +667,169 @@ class PangeaMessageEvent {
}
/// Returns a list of [PracticeActivityEvent] for the user's active l2.
List<PracticeActivityEvent> get practiceActivities {
final String? l2code =
MatrixState.pangeaController.languageController.activeL2Code();
if (l2code == null) return [];
return practiceActivitiesByLangCode(l2code);
List<PracticeActivityEvent> get practiceActivities =>
l2Code == null ? [] : practiceActivitiesByLangCode(l2Code!);
/// all construct uses for the message, including vocab and grammar
List<OneConstructUse> get allConstructUses =>
[..._grammarConstructUses, ..._vocabUses, ..._itStepsToConstructUses];
/// Returns a list of [OneConstructUse] from itSteps for which the continuance
/// was selected or ignored. Correct selections are considered in the tokens
/// flow. Once all continuances have lemmas, we can do both correct and incorrect
/// in this flow. It actually doesn't do anything at all right now, because the
/// choregrapher is not returning lemmas for continuances. This is a TODO.
/// So currently only the lemmas can be gotten from the tokens for choices that
/// are actually in the final message.
List<OneConstructUse> get _itStepsToConstructUses {
final List<OneConstructUse> uses = [];
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;
}
/// get construct uses of type vocab for the message
List<OneConstructUse> get _vocabUses {
final List<OneConstructUse> uses = [];
// missing vital info so return
if (event.roomId == null || originalSent?.tokens == null) {
debugger(when: kDebugMode);
return uses;
}
// for each token, record whether selected in ga, ta, or wa
for (final token in originalSent!.tokens!) {
uses.addAll(_getVocabUseForToken(token));
}
return uses;
}
/// Returns a list of [OneConstructUse] objects for the given [token]
/// If there is no [originalSent] or [originalSent.choreo], the [token] is
/// considered to be a [ConstructUseTypeEnum.wa] as long as it matches the target language.
/// Later on, we may want to consider putting it in some category of like 'pending'
/// If the [token] is in the [originalSent.choreo.acceptedOrIgnoredMatch],
/// it is considered to be a [ConstructUseTypeEnum.ga].
/// If the [token] is in the [originalSent.choreo.acceptedOrIgnoredMatch.choices],
/// it is considered to be a [ConstructUseTypeEnum.corIt].
/// If the [token] is not included in any choreoStep, it is considered to be a [ConstructUseTypeEnum.wa].
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);
}
// 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
/// Convert a list of [lemmas] into a list of vocab uses
/// with the given [type]
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;
}
// replication of logic from message_content.dart
// bool get isHtml =>
// AppConfig.renderHtml && !_event.redacted && _event.isRichMessage;
/// get construct uses of type grammar for the message
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 {

@ -12,7 +12,7 @@ import 'package:matrix/src/utils/markdown.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../../widgets/matrix.dart';
import '../constants/language_keys.dart';
import '../constants/language_constants.dart';
import '../constants/pangea_event_types.dart';
import '../models/choreo_record.dart';
import '../models/representation_content_model.dart';

@ -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!;
}
}

@ -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';
@ -71,7 +71,9 @@ class PracticeActivityEvent {
return records.firstOrNull;
}
/// Checks if there is a user record for this activity,
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;
}

@ -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;
}
}
}

@ -1,8 +1,6 @@
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
import 'package:matrix/matrix.dart';
@ -28,32 +26,4 @@ abstract class AnalyticsEvent {
}
return contentCache!;
}
static List<String> analyticsEventTypes = [
PangeaEventTypes.summaryAnalytics,
PangeaEventTypes.construct,
];
static Future<String?> sendEvent(
Room analyticsRoom,
String type,
List<dynamic> analyticsContent,
) async {
String? eventId;
switch (type) {
case PangeaEventTypes.summaryAnalytics:
eventId = await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent(
analyticsRoom,
analyticsContent.cast<RecentMessageRecord>(),
);
break;
case PangeaEventTypes.construct:
eventId = await ConstructAnalyticsEvent.sendConstructsEvent(
analyticsRoom,
analyticsContent.cast<OneConstructUse>(),
);
break;
}
return eventId;
}
}

@ -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 [];
}

@ -18,19 +18,4 @@ class ConstructAnalyticsEvent extends AnalyticsEvent {
contentCache ??= ConstructAnalyticsModel.fromJson(event.content);
return contentCache as ConstructAnalyticsModel;
}
static Future<String?> sendConstructsEvent(
Room analyticsRoom,
List<OneConstructUse> uses,
) async {
final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel(
uses: uses,
);
final String? eventId = await analyticsRoom.sendEvent(
constructsModel.toJson(),
type: PangeaEventTypes.construct,
);
return eventId;
}
}

@ -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({

@ -18,18 +18,4 @@ class SummaryAnalyticsEvent extends AnalyticsEvent {
contentCache ??= SummaryAnalyticsModel.fromJson(event.content);
return contentCache as SummaryAnalyticsModel;
}
static Future<String?> sendSummaryAnalyticsEvent(
Room analyticsRoom,
List<RecentMessageRecord> records,
) async {
final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel(
messages: records,
);
final String? eventId = await analyticsRoom.sendEvent(
analyticsModel.toJson(),
type: PangeaEventTypes.summaryAnalytics,
);
return eventId;
}
}

@ -50,7 +50,7 @@ class SummaryAnalyticsModel extends AnalyticsModel {
(msg) => RecentMessageRecord(
eventId: msg.eventId,
chatId: msg.room.id,
useType: msg.useType,
useType: msg.msgUseType,
time: msg.originServerTs,
),
)

@ -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();

@ -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;
}
}

@ -1,5 +1,6 @@
import 'dart:developer';
import 'package:fluffychat/pangea/controllers/language_detection_controller.dart';
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/models/span_card_model.dart';
@ -13,12 +14,11 @@ import 'package:matrix/matrix.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import '../constants/model_keys.dart';
import 'language_detection_model.dart';
// import 'package:language_tool/language_tool.dart';
class IGCTextData {
List<LanguageDetection> detections;
LanguageDetectionResponse detections;
String originalInput;
String? fullTextCorrection;
List<PangeaToken> tokens;
@ -42,6 +42,18 @@ class IGCTextData {
});
factory IGCTextData.fromJson(Map<String, dynamic> json) {
// changing this to allow for use of the LanguageDetectionResponse methods
// TODO - change API after we're sure all clients are updated. not urgent.
final LanguageDetectionResponse detections =
json[_detectionsKey] is Iterable
? LanguageDetectionResponse.fromJson({
"detections": json[_detectionsKey],
"full_text": json["original_input"],
})
: LanguageDetectionResponse.fromJson(
json[_detectionsKey] as Map<String, dynamic>,
);
return IGCTextData(
tokens: (json[_tokensKey] as Iterable)
.map<PangeaToken>(
@ -59,12 +71,7 @@ class IGCTextData {
.toList()
.cast<PangeaMatch>()
: [],
detections: (json[_detectionsKey] as Iterable)
.map<LanguageDetection>(
(e) => LanguageDetection.fromJson(e as Map<String, dynamic>),
)
.toList()
.cast<LanguageDetection>(),
detections: detections,
originalInput: json["original_input"],
fullTextCorrection: json["full_text_correction"],
userL1: json[ModelKey.userL1],
@ -79,7 +86,7 @@ class IGCTextData {
static const String _detectionsKey = "detections";
Map<String, dynamic> toJson() => {
_detectionsKey: detections.map((e) => e.toJson()).toList(),
_detectionsKey: detections.toJson(),
"original_input": originalInput,
"full_text_correction": fullTextCorrection,
_tokensKey: tokens.map((e) => e.toJson()).toList(),
@ -90,6 +97,18 @@ class IGCTextData {
"enable_igc": enableIGC,
};
/// if we haven't run IGC or IT or there are no matches, we use the highest validated detection
/// from [LanguageDetectionResponse.highestValidatedDetection]
/// if we have run igc/it and there are no matches, we can relax the threshold
/// and use the highest confidence detection
String get detectedLanguage {
if (!(enableIGC && enableIT) || matches.isNotEmpty) {
return detections.highestValidatedDetection().langCode;
} else {
return detections.highestConfidenceDetection.langCode;
}
}
// reconstruct fullText based on accepted match
//update offsets in existing matches to reflect the change
//if existing matches overlap with the accepted one, remove them??

@ -1,6 +1,6 @@
import 'dart:developer';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';

@ -1,6 +1,13 @@
/// Represents a lemma object
class Lemma {
/// [text] ex "ir" - text of the lemma of the word
final String text;
/// [form] ex "vamos" - conjugated form of the lemma and as it appeared in some original text
final String form;
/// [saveVocab] true - whether to save the lemma to the user's vocabulary
/// vocab that are not saved: emails, urls, numbers, punctuation, etc.
final bool saveVocab;
Lemma({required this.text, required this.saveVocab, required this.form});

@ -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'],
),
);

@ -5,16 +5,18 @@
import 'dart:developer';
import 'dart:typed_data';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
class PracticeActivityRecordModel {
final String? question;
late List<ActivityResponse> responses;
late List<ActivityRecordResponse> responses;
PracticeActivityRecordModel({
required this.question,
List<ActivityResponse>? responses,
List<ActivityRecordResponse>? responses,
}) {
if (responses == null) {
this.responses = List<ActivityResponse>.empty(growable: true);
this.responses = List<ActivityRecordResponse>.empty(growable: true);
} else {
this.responses = responses;
}
@ -26,7 +28,9 @@ class PracticeActivityRecordModel {
return PracticeActivityRecordModel(
question: json['question'] as String,
responses: (json['responses'] as List)
.map((e) => ActivityResponse.fromJson(e as Map<String, dynamic>))
.map(
(e) => ActivityRecordResponse.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
}
@ -40,26 +44,34 @@ 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(
ActivityResponse(
ActivityRecordResponse(
text: text,
audioBytes: audioBytes,
imageBytes: imageBytes,
timestamp: DateTime.now(),
score: score,
),
);
} catch (e) {
@ -84,27 +96,33 @@ class PracticeActivityRecordModel {
int get hashCode => question.hashCode ^ responses.hashCode;
}
class ActivityResponse {
class ActivityRecordResponse {
// 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;
ActivityResponse({
ActivityRecordResponse({
this.text,
this.audioBytes,
this.imageBytes,
required this.score,
required this.timestamp,
});
factory ActivityResponse.fromJson(Map<String, dynamic> json) {
return ActivityResponse(
factory ActivityRecordResponse.fromJson(Map<String, dynamic> json) {
return ActivityRecordResponse(
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,
);
}
@ -114,6 +132,7 @@ class ActivityResponse {
'audio': audioBytes,
'image': imageBytes,
'timestamp': timestamp.toIso8601String(),
'score': score,
};
}
@ -121,7 +140,7 @@ class ActivityResponse {
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ActivityResponse &&
return other is ActivityRecordResponse &&
other.text == text &&
other.audioBytes == audioBytes &&
other.imageBytes == imageBytes &&

@ -7,7 +7,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
import '../constants/class_default_values.dart';
import '../constants/language_keys.dart';
import '../constants/language_constants.dart';
import '../constants/pangea_event_types.dart';
import 'language_model.dart';

@ -9,7 +9,7 @@ import 'package:fluffychat/pangea/utils/instructions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../constants/language_keys.dart';
import '../constants/language_constants.dart';
import 'language_model.dart';
PUserModel pUserModelFromJson(String str) =>

@ -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,

@ -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 = [];

@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:developer';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
@ -29,49 +28,35 @@ class StudentAnalyticsPage extends StatefulWidget {
class StudentAnalyticsController extends State<StudentAnalyticsPage> {
final PangeaController _pangeaController = MatrixState.pangeaController;
AnalyticsSelected? selected;
StreamSubscription? stateSub;
@override
void initState() {
super.initState();
final listFutures = [
_pangeaController.myAnalytics.setStudentChats(),
_pangeaController.myAnalytics.setStudentSpaces(),
];
Future.wait(listFutures).then((_) => setState(() {}));
stateSub = _pangeaController.myAnalytics.stateStream.listen((_) {
setState(() {});
});
}
@override
void dispose() {
stateSub?.cancel();
super.dispose();
}
List<Room> _chats = [];
List<Room> get chats {
if (_pangeaController.myAnalytics.studentChats.isEmpty) {
_pangeaController.myAnalytics.setStudentChats().then((_) {
if (_pangeaController.myAnalytics.studentChats.isNotEmpty) {
setState(() {});
}
if (_chats.isEmpty) {
_pangeaController.matrixState.client.chatsImAStudentIn.then((result) {
setState(() => _chats = result);
});
}
return _pangeaController.myAnalytics.studentChats;
return _chats;
}
List<Room> _spaces = [];
List<Room> get spaces {
if (_pangeaController.myAnalytics.studentSpaces.isEmpty) {
_pangeaController.myAnalytics.setStudentSpaces().then((_) {
if (_pangeaController.myAnalytics.studentSpaces.isNotEmpty) {
setState(() {});
}
if (_spaces.isEmpty) {
_pangeaController.matrixState.client.spaceImAStudentIn.then((result) {
setState(() => _spaces = result);
});
}
return _pangeaController.myAnalytics.studentSpaces;
return _spaces;
}
String? get userId {

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:fluffychat/pangea/config/environment.dart';
import 'package:fluffychat/pangea/controllers/language_detection_controller.dart';
import 'package:fluffychat/pangea/models/language_detection_model.dart';
import 'package:fluffychat/pangea/models/lemma.dart';
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
@ -39,7 +40,10 @@ class IgcRepo {
await Future.delayed(const Duration(seconds: 2));
final IGCTextData igcTextData = IGCTextData(
detections: [LanguageDetection(langCode: "en", confidence: 0.99)],
detections: LanguageDetectionResponse(
detections: [LanguageDetection(langCode: "en", confidence: 0.99)],
fullText: "This be a sample text",
),
tokens: [
PangeaToken(
text: PangeaTokenText(content: "This", offset: 0, length: 4),
@ -89,7 +93,6 @@ class IGCRequestBody {
String fullText;
String userL1;
String userL2;
bool tokensOnly;
bool enableIT;
bool enableIGC;
@ -99,7 +102,6 @@ class IGCRequestBody {
required this.userL2,
required this.enableIGC,
required this.enableIT,
this.tokensOnly = false,
});
Map<String, dynamic> toJson() => {
@ -108,6 +110,5 @@ class IGCRequestBody {
ModelKey.userL2: userL2,
"enable_it": enableIT,
"enable_igc": enableIGC,
"tokens_only": tokensOnly,
};
}

@ -4,7 +4,6 @@ import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
import 'package:flutter/widgets.dart';
import '../../config/firebase_options.dart';
import '../enum/use_type.dart';
// PageRoute import
@ -90,13 +89,12 @@ class GoogleAnalytics {
logEvent('join_group', parameters: {'group_id': classCode});
}
static sendMessage(String chatRoomId, String classCode, UseType useType) {
static sendMessage(String chatRoomId, String classCode) {
logEvent(
'sent_message',
parameters: {
"chat_id": chatRoomId,
'group_id': classCode,
"message_type": useType.toString(),
},
);
}

@ -1,5 +1,5 @@
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';

@ -166,7 +166,7 @@ class OverlayMessage extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
if (pangeaMessageEvent.showUseType) ...[
pangeaMessageEvent.useType.iconView(
pangeaMessageEvent.msgUseType.iconView(
context,
textColor.withAlpha(164),
),

@ -1,6 +1,6 @@
import 'dart:developer';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/models/language_model.dart';

@ -1,9 +1,8 @@
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../models/chat_topic_model.dart';
import '../../models/lemma.dart';
import '../../repo/topic_data_repo.dart';
@ -76,7 +75,7 @@ class ChatVocabularyList extends StatelessWidget {
for (final word in topic.vocab)
Chip(
labelStyle: Theme.of(context).textTheme.bodyMedium,
label: Text(word.form),
label: Text(word.text),
onDeleted: () {
onChanged(topic.vocab..remove(word));
},
@ -464,7 +463,7 @@ class PromptsFieldState extends State<PromptsField> {
// button to call API
ElevatedButton.icon(
icon: BotFace(
icon: const BotFace(
width: 50.0,
expression: BotExpression.idle,
),

@ -28,7 +28,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
widget.controller.currentRecordModel;
bool get isSubmitted =>
widget.currentActivity?.userRecord?.record?.latestResponse != null;
widget.currentActivity?.userRecord?.record.latestResponse != null;
@override
void initState() {
@ -64,15 +64,19 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
.setCurrentModel(widget.currentActivity!.userRecord!.record);
selectedChoiceIndex = widget
.currentActivity?.practiceActivity.multipleChoice!
.choiceIndex(currentRecordModel!.latestResponse!);
.choiceIndex(currentRecordModel!.latestResponse!.text!);
}
setState(() {});
}
void updateChoice(int index) {
currentRecordModel?.addResponse(
text: widget.controller.currentActivity?.practiceActivity.multipleChoice!
text: widget.controller.currentActivity!.practiceActivity.multipleChoice!
.choices[index],
score: widget.controller.currentActivity!.practiceActivity.multipleChoice!
.isCorrect(index)
? 1
: 0,
);
setState(() => selectedChoiceIndex = index);
}

@ -1,4 +1,4 @@
import 'package:fluffychat/pangea/constants/language_level_type.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/utils/language_level_copy.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';

@ -1,6 +1,6 @@
import 'dart:developer';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/models/language_model.dart';

@ -24,7 +24,7 @@ import 'dart:io';
import 'package:fcm_shared_isolate/fcm_shared_isolate.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:fluffychat/pangea/constants/language_keys.dart';
import 'package:fluffychat/pangea/constants/language_constants.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/utils/push_helper.dart';
import 'package:fluffychat/widgets/fluffy_chat_app.dart';

@ -29,6 +29,7 @@ import pasteboard
import path_provider_foundation
import purchases_flutter
import record_darwin
import rive_common
import sentry_flutter
import share_plus
import shared_preferences_foundation
@ -65,6 +66,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
PurchasesFlutterPlugin.register(with: registry.registrar(forPlugin: "PurchasesFlutterPlugin"))
RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin"))
RivePlugin.register(with: registry.registrar(forPlugin: "RivePlugin"))
SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

@ -15,6 +15,7 @@
#include <pasteboard/pasteboard_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <record_windows/record_windows_plugin_c_api.h>
#include <rive_common/rive_plugin.h>
#include <sentry_flutter/sentry_flutter_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlcipher_flutter_libs/sqlite3_flutter_libs_plugin.h>
@ -40,6 +41,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
RecordWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
RivePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RivePlugin"));
SentryFlutterPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SentryFlutterPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(

@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
pasteboard
permission_handler_windows
record_windows
rive_common
sentry_flutter
share_plus
sqlcipher_flutter_libs

Loading…
Cancel
Save