moved inline tooltip to seperate widget, changed text type and icon for inline tooltip

This commit is contained in:
ggurdin 2024-07-01 14:10:36 -04:00
commit 64ff39f9e2
82 changed files with 1151 additions and 55997 deletions

View file

@ -79,5 +79,7 @@ jobs:
with:
name: web
path: build/web
- name: Update packages
run: flutter pub get
- name: Update sentry
run: flutter packages pub run sentry_dart_plugin

View file

@ -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!",
@ -4067,6 +4067,7 @@
"noActivitiesFound": "No practice activities found for this message",
"hintTitle": "Hint:",
"speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores",
"previous": "Previous",
"languageButtonLabel": "Language: {currentLanguage}",
"@languageButtonLabel": {
"type": "text",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,13 @@ class PangeaEventTypes {
static const String report = 'm.report';
static const textToSpeechRule = "p.rule.text_to_speech";
static const pangeaActivityRes = "pangea.activity_res";
static const acitivtyRequest = "pangea.activity_req";
/// A request to the server to generate activities
static const activityRequest = "pangea.activity_req";
/// A practice activity that is related to a message
static const pangeaActivity = "pangea.activity_res";
/// A record of completion of an activity. There
/// can be one per user per activity.
static const activityRecord = "pangea.activity_completion";
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,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();
// if analytics haven't been updated in the last day, update them
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
// 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);
});
}
/// given an update from sync stream, check if the update contains
/// 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);
}
return lastUpdated;
}
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,193 +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();
// analytics room for the user and current target language
final Room analyticsRoom = await _client.getMyAnalyticsRoom(userL2!);
// get the last updated time for each analytics room
// and the least recent update, which will be used to determine
// how far to go back in the chat history to get messages
final Map<String, DateTime?> lastUpdatedMap = await _pangeaController
.matrixState.client
.allAnalyticsRoomsLastUpdated();
final List<DateTime> lastUpdates = lastUpdatedMap.values
.where((lastUpdate) => lastUpdate != null)
.cast<DateTime>()
.toList();
/// Get the last time that analytics to for current target language
/// were updated. This my present a problem is the user has analytics
/// rooms for multiple languages, and a non-target language was updated
/// less recently than the target language. In this case, some data may
/// be missing, but a case like that seems relatively rare, and could
/// result in unnecessaily going too far back in the chat history
DateTime? l2AnalyticsLastUpdated = lastUpdatedMap[userL2];
if (l2AnalyticsLastUpdated == null) {
/// if the target language has never been updated, use the least
/// recent update time
lastUpdates.sort((a, b) => a.compareTo(b));
l2AnalyticsLastUpdated =
lastUpdates.isNotEmpty ? lastUpdates.first : null;
}
// 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);
// all chats in which user is a student
final List<Room> chats = _client.rooms
.where((room) => !room.isSpace && !room.isAnalyticsRoom)
.toList();
// 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,
// 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,
),
);
}
}
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();
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);
}
// get the timelines for each chat
final List<Future<Timeline>> timelineFutures = [];
for (final chat in chats) {
timelineFutures.add(chat.getTimeline());
}
return langCodeToMsgs;
}
final List<Timeline> timelines = await Future.wait(timelineFutures);
final Map<String, Timeline> timelineMap =
Map.fromIterables(chats.map((e) => e.id), timelines);
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),
//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<PangeaMessageEvent> allRecentMessages =
recentPangeaMessageEvents.expand((e) => e).toList();
final List<RecentMessageRecord> summaryContent =
SummaryAnalyticsModel.formatSummaryContent(recentMsgs);
final List<OneConstructUse> constructContent =
ConstructAnalyticsModel.formatConstructsContent(recentMsgs);
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);
}
// 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));
}
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,
);
}
}
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 [];
}
}
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 [];
}
}
}

View file

@ -51,7 +51,7 @@ class PracticeGenerationController {
final Event? activityEvent = await pangeaMessageEvent.room.sendPangeaEvent(
content: model.toJson(),
parentEventId: pangeaMessageEvent.eventId,
type: PangeaEventTypes.pangeaActivityRes,
type: PangeaEventTypes.pangeaActivity,
);
if (activityEvent == null) {
@ -88,7 +88,7 @@ class PracticeGenerationController {
PracticeActivityModel dummyModel(PangeaMessageEvent event) =>
PracticeActivityModel(
tgtConstructs: [
ConstructIdentifier(lemma: "be", type: ConstructType.vocab),
ConstructIdentifier(lemma: "be", type: ConstructTypeEnum.vocab),
],
activityType: ActivityTypeEnum.multipleChoice,
langCode: event.messageDisplayLangCode,

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,3 @@
import 'package:fluffychat/pangea/utils/bot_style.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -43,38 +42,4 @@ extension Copy on InstructionsEnum {
: L10n.of(context)!.tooltipInstructionsBrowserBody;
}
}
Widget inlineTooltip(BuildContext context) {
switch (this) {
case InstructionsEnum.speechToText:
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.record_voice_over_outlined,
size: 20,
),
const SizedBox(
width: 7,
),
Text(
title(context),
style: BotStyle.text(context),
),
],
),
Text(
body(context),
style: BotStyle.text(context),
),
// ),
],
);
default:
debugPrint('inlineTooltip not implemented for $this');
return const SizedBox();
}
}
}

View file

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

View file

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

View file

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

View file

@ -28,7 +28,7 @@ extension PangeaEvent on Event {
return PangeaRepresentation.fromJson(json) as V;
case PangeaEventTypes.choreoRecord:
return ChoreoRecord.fromJson(json) as V;
case PangeaEventTypes.pangeaActivityRes:
case PangeaEventTypes.pangeaActivity:
return PracticeActivityModel.fromJson(json) as V;
case PangeaEventTypes.activityRecord:
return PracticeActivityRecordModel.fromJson(json) as V;

View file

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

View file

@ -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) =>

View file

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

View file

@ -2,14 +2,20 @@ import 'dart:convert';
import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/constants/model_keys.dart';
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/choreo_record.dart';
import 'package:fluffychat/pangea/models/lemma.dart';
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/models/representation_content_model.dart';
import 'package:fluffychat/pangea/models/space_model.dart';
import 'package:fluffychat/pangea/models/speech_to_text_models.dart';
@ -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 &&
@ -566,18 +582,8 @@ class PangeaMessageEvent {
/// If any activity is not complete, it returns true, indicating that the activity icon should be shown.
/// Otherwise, it returns false.
bool get hasUncompletedActivity {
if (l2Code == null) return false;
final List<PracticeActivityEvent> activities = practiceActivities(l2Code!);
if (activities.isEmpty) return false;
// for now, only show the button if the event has no completed activities
// TODO - revert this after adding logic to show next activity
for (final activity in activities) {
if (activity.isComplete) return false;
}
return true;
// if (activities.isEmpty) return false;
// return !activities.every((activity) => activity.isComplete);
if (practiceActivities.isEmpty) return false;
return practiceActivities.any((activity) => !(activity.isComplete));
}
String? get l2Code =>
@ -611,34 +617,36 @@ class PangeaMessageEvent {
return steps;
}
List<PracticeActivityEvent> get _practiceActivityEvents => _latestEdit
.aggregatedEvents(
timeline,
PangeaEventTypes.pangeaActivityRes,
)
.map(
(e) => PracticeActivityEvent(
timeline: timeline,
event: e,
),
)
.toList();
/// Returns a list of all [PracticeActivityEvent] objects
/// associated with this message event.
List<PracticeActivityEvent> get _practiceActivityEvents {
return _latestEdit
.aggregatedEvents(
timeline,
PangeaEventTypes.pangeaActivity,
)
.map(
(e) => PracticeActivityEvent(
timeline: timeline,
event: e,
),
)
.toList();
}
/// Returns a boolean value indicating whether there are any
/// activities associated with this message event for the user's active l2
bool get hasActivities {
try {
final String? l2code =
MatrixState.pangeaController.languageController.activeL2Code();
if (l2code == null) return false;
return practiceActivities(l2code).isNotEmpty;
return practiceActivities.isNotEmpty;
} catch (e, s) {
ErrorHandler.logError(e: e, s: s);
return false;
}
}
List<PracticeActivityEvent> practiceActivities(
/// Returns a list of [PracticeActivityEvent] objects for the given [langCode].
List<PracticeActivityEvent> practiceActivitiesByLangCode(
String langCode, {
bool debug = false,
}) {
@ -658,14 +666,170 @@ class PangeaMessageEvent {
}
}
// List<SpanData> get activities =>
//each match is turned into an activity that other students can access
//they're not told the answer but have to find it themselves
//the message has a blank piece which they fill in themselves
/// Returns a list of [PracticeActivityEvent] for the user's active l2.
List<PracticeActivityEvent> get practiceActivities =>
l2Code == null ? [] : practiceActivitiesByLangCode(l2Code!);
// replication of logic from message_content.dart
// bool get isHtml =>
// AppConfig.renderHtml && !_event.redacted && _event.isRichMessage;
/// 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);
}
/// 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;
}
/// 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 {

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import 'dart:developer';
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
@ -27,7 +27,7 @@ class PracticeActivityEvent {
_content = content;
}
}
if (event.type != PangeaEventTypes.pangeaActivityRes) {
if (event.type != PangeaEventTypes.pangeaActivity) {
throw Exception(
"${event.type} should not be used to make a PracticeActivityEvent",
);
@ -39,7 +39,7 @@ class PracticeActivityEvent {
return _content!;
}
//in aggregatedEvents for the event, find all practiceActivityRecordEvents whose sender matches the client's userId
/// All completion records assosiated with this activity
List<PracticeActivityRecordEvent> get allRecords {
if (timeline == null) {
debugger(when: kDebugMode);
@ -54,14 +54,26 @@ class PracticeActivityEvent {
.toList();
}
List<PracticeActivityRecordEvent> get userRecords => allRecords
.where(
(recordEvent) =>
recordEvent.event.senderId == recordEvent.event.room.client.userID,
)
.toList();
/// Completion record assosiated with this activity
/// for the logged in user, null if there is none
PracticeActivityRecordEvent? get userRecord {
final List<PracticeActivityRecordEvent> records = allRecords
.where(
(recordEvent) =>
recordEvent.event.senderId ==
recordEvent.event.room.client.userID,
)
.toList();
if (records.length > 1) {
debugPrint("There should only be one record per user per activity");
debugger(when: kDebugMode);
}
return records.firstOrNull;
}
String get parentMessageId => event.relationshipEventId!;
/// Checks if there are any user records in the list for this activity,
/// and, if so, then the activity is complete
bool get isComplete => userRecords.isNotEmpty;
bool get isComplete => userRecord != null;
}

View file

@ -0,0 +1,89 @@
import 'dart:developer';
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:matrix/matrix.dart';
import '../constants/pangea_event_types.dart';
class PracticeActivityRecordEvent {
Event event;
PracticeActivityRecordModel? _content;
PracticeActivityRecordEvent({required this.event}) {
if (event.type != PangeaEventTypes.activityRecord) {
throw Exception(
"${event.type} should not be used to make a PracticeActivityRecordEvent",
);
}
}
PracticeActivityRecordModel get record {
_content ??= event.getPangeaContent<PracticeActivityRecordModel>();
return _content!;
}
Future<List<OneConstructUse>> uses(Timeline timeline) async {
try {
final String? parent = event.relationshipEventId;
if (parent == null) {
debugger(when: kDebugMode);
ErrorHandler.logError(
m: "PracticeActivityRecordEvent has null event.relationshipEventId",
data: event.toJson(),
);
return [];
}
final Event? practiceEvent =
await timeline.getEventById(event.relationshipEventId!);
if (practiceEvent == null) {
debugger(when: kDebugMode);
ErrorHandler.logError(
m: "PracticeActivityRecordEvent has null practiceActivityEvent with id $parent",
data: event.toJson(),
);
return [];
}
final PracticeActivityEvent practiceActivity = PracticeActivityEvent(
event: practiceEvent,
timeline: timeline,
);
final List<OneConstructUse> uses = [];
final List<ConstructIdentifier> constructIds =
practiceActivity.practiceActivity.tgtConstructs;
for (final construct in constructIds) {
uses.add(
OneConstructUse(
lemma: construct.lemma,
constructType: construct.type,
useType: record.useType,
//TODO - find form of construct within the message
//this is related to the feature of highlighting the target construct in the message
form: construct.lemma,
chatId: event.roomId ?? practiceEvent.roomId ?? timeline.room.id,
msgId: practiceActivity.parentMessageId,
timeStamp: event.originServerTs,
),
);
}
return uses;
} catch (e, s) {
debugger(when: kDebugMode);
ErrorHandler.logError(e: e, s: s, data: event.toJson());
rethrow;
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,7 +27,7 @@ class MultipleChoice {
return MultipleChoice(
question: json['question'] as String,
choices: (json['choices'] as List).map((e) => e as String).toList(),
answer: json['answer'] as String,
answer: json['answer'] ?? json['correct_answer'] as String,
);
}

View file

@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart';
class ConstructIdentifier {
final String lemma;
final ConstructType type;
final ConstructTypeEnum type;
ConstructIdentifier({required this.lemma, required this.type});
@ -16,7 +16,7 @@ class ConstructIdentifier {
try {
return ConstructIdentifier(
lemma: json['lemma'] as String,
type: ConstructType.values.firstWhere(
type: ConstructTypeEnum.values.firstWhere(
(e) => e.string == json['type'],
),
);
@ -243,9 +243,11 @@ class PracticeActivityModel {
.toList(),
langCode: json['lang_code'] as String,
msgId: json['msg_id'] as String,
activityType: ActivityTypeEnum.values.firstWhere(
(e) => e.string == json['activity_type'],
),
activityType: json['activity_type'] == "multipleChoice"
? ActivityTypeEnum.multipleChoice
: ActivityTypeEnum.values.firstWhere(
(e) => e.string == json['activity_type'],
),
multipleChoice: json['multiple_choice'] != null
? MultipleChoice.fromJson(
json['multiple_choice'] as Map<String, dynamic>,

View file

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

View file

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

View file

@ -9,7 +9,7 @@ import 'package:fluffychat/pangea/models/space_model.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) =>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
import 'package:fluffychat/pangea/utils/inline_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -35,6 +36,9 @@ class InstructionsController {
bool wereInstructionsTurnedOff(InstructionsEnum key) =>
toggledOff(key) ?? _instructionsClosed[key] ?? false;
void turnOffInstruction(InstructionsEnum key) =>
_instructionsClosed[key] = true;
Future<void> updateEnableInstructions(
InstructionsEnum key,
bool value,
@ -112,16 +116,15 @@ class InstructionsController {
/// Returns a widget that will be added to existing widget
/// which displays hint text defined in the enum extension
Widget getInlineTooltip(
Widget getInstructionInlineTooltip(
BuildContext context,
InstructionsEnum key,
Function refreshOnClose,
VoidCallback onClose,
) {
if (wereInstructionsTurnedOff(key)) {
// Uncomment this line to make hint viewable again
// _instructionsClosed[key] = false;
return const SizedBox();
}
if (L10n.of(context) == null) {
ErrorHandler.logError(
m: "null context in ITBotButton.showCard",
@ -129,39 +132,10 @@ class InstructionsController {
);
return const SizedBox();
}
return Badge(
offset: const Offset(0, -7),
backgroundColor: Colors.transparent,
label: CircleAvatar(
radius: 10,
backgroundColor: Theme.of(context).colorScheme.primary.withAlpha(20),
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(
Icons.close_outlined,
size: 15,
),
onPressed: () {
_instructionsClosed[key] = true;
refreshOnClose();
},
),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
10,
),
color: Theme.of(context).colorScheme.primary.withAlpha(20),
// border: Border.all(
// color: Theme.of(context).colorScheme.primary.withAlpha(50),
// ),
),
child: Padding(
padding: const EdgeInsets.all(10),
child: key.inlineTooltip(context),
),
),
return InlineTooltip(
body: InstructionsEnum.speechToText.body(context),
onClose: onClose,
);
}
}

View file

@ -65,7 +65,10 @@ class MessageSpeechToTextCardState extends State<MessageSpeechToTextCard> {
}
}
void refreshOnCloseHint() {
void closeHint() {
MatrixState.pangeaController.instructions.turnOffInstruction(
InstructionsEnum.speechToText,
);
setState(() {});
}
@ -199,10 +202,10 @@ class MessageSpeechToTextCardState extends State<MessageSpeechToTextCard> {
),
],
),
MatrixState.pangeaController.instructions.getInlineTooltip(
MatrixState.pangeaController.instructions.getInstructionInlineTooltip(
context,
InstructionsEnum.speechToText,
refreshOnCloseHint,
closeHint,
),
],
);

View file

@ -307,7 +307,6 @@ class MessageToolbarState extends State<MessageToolbar> {
void showPracticeActivity() {
toolbarContent = PracticeActivityCard(
pangeaMessageEvent: widget.pangeaMessageEvent,
controller: this,
);
}

View file

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

View file

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

View file

@ -1,9 +1,8 @@
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 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.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,
),

View file

@ -2,31 +2,93 @@ import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_content.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
import 'package:flutter/material.dart';
class MultipleChoiceActivity extends StatelessWidget {
final MessagePracticeActivityContentState card;
final Function(int) updateChoice;
final bool isActive;
/// The multiple choice activity view
class MultipleChoiceActivity extends StatefulWidget {
final MessagePracticeActivityCardState controller;
final PracticeActivityEvent? currentActivity;
const MultipleChoiceActivity({
super.key,
required this.card,
required this.updateChoice,
required this.isActive,
required this.controller,
required this.currentActivity,
});
PracticeActivityEvent get practiceEvent => card.practiceEvent;
@override
MultipleChoiceActivityState createState() => MultipleChoiceActivityState();
}
int? get selectedChoiceIndex => card.selectedChoiceIndex;
class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
int? selectedChoiceIndex;
bool get submitted => card.recordSubmittedThisSession;
PracticeActivityRecordModel? get currentRecordModel =>
widget.controller.currentRecordModel;
bool get isSubmitted =>
widget.currentActivity?.userRecord?.record.latestResponse != null;
@override
void initState() {
super.initState();
setCompletionRecord();
}
@override
void didUpdateWidget(covariant MultipleChoiceActivity oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.currentActivity?.event.eventId !=
widget.currentActivity?.event.eventId) {
setCompletionRecord();
}
}
/// Sets the completion record for the multiple choice activity.
/// If the user record is null, it creates a new record model with the question
/// from the current activity and sets the selected choice index to null.
/// Otherwise, it sets the current model to the user record's record and
/// determines the selected choice index.
void setCompletionRecord() {
if (widget.currentActivity?.userRecord?.record == null) {
widget.controller.setCurrentModel(
PracticeActivityRecordModel(
question:
widget.currentActivity?.practiceActivity.multipleChoice!.question,
),
);
selectedChoiceIndex = null;
} else {
widget.controller
.setCurrentModel(widget.currentActivity!.userRecord!.record);
selectedChoiceIndex = widget
.currentActivity?.practiceActivity.multipleChoice!
.choiceIndex(currentRecordModel!.latestResponse!.text!);
}
setState(() {});
}
void updateChoice(int index) {
currentRecordModel?.addResponse(
text: widget.controller.currentActivity!.practiceActivity.multipleChoice!
.choices[index],
score: widget.controller.currentActivity!.practiceActivity.multipleChoice!
.isCorrect(index)
? 1
: 0,
);
setState(() => selectedChoiceIndex = index);
}
@override
Widget build(BuildContext context) {
final PracticeActivityModel practiceActivity =
practiceEvent.practiceActivity;
final PracticeActivityModel? practiceActivity =
widget.currentActivity?.practiceActivity;
if (practiceActivity == null) {
return const SizedBox();
}
return Container(
padding: const EdgeInsets.all(8),
@ -50,17 +112,14 @@ class MultipleChoiceActivity extends StatelessWidget {
.mapIndexed(
(index, value) => Choice(
text: value,
color: (selectedChoiceIndex == index ||
practiceActivity.multipleChoice!
.isCorrect(index)) &&
submitted
color: selectedChoiceIndex == index
? practiceActivity.multipleChoice!.choiceColor(index)
: null,
isGold: practiceActivity.multipleChoice!.isCorrect(index),
),
)
.toList(),
isActive: isActive,
isActive: !isSubmitted,
),
],
),

View file

@ -1,24 +1,24 @@
import 'dart:developer';
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
import 'package:fluffychat/pangea/utils/bot_style.dart';
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_content.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
/// The wrapper for practice activity content.
/// Handles the activities assosiated with a message,
/// their navigation, and the management of completion records
class PracticeActivityCard extends StatefulWidget {
final PangeaMessageEvent pangeaMessageEvent;
final MessageToolbarState controller;
const PracticeActivityCard({
super.key,
required this.pangeaMessageEvent,
required this.controller,
});
@override
@ -27,69 +27,129 @@ class PracticeActivityCard extends StatefulWidget {
}
class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
PracticeActivityEvent? practiceEvent;
PracticeActivityEvent? currentActivity;
PracticeActivityRecordModel? currentRecordModel;
bool sending = false;
List<PracticeActivityEvent> get practiceActivities =>
widget.pangeaMessageEvent.practiceActivities;
int get practiceEventIndex => practiceActivities.indexWhere(
(activity) => activity.event.eventId == currentActivity?.event.eventId,
);
bool get isPrevEnabled =>
practiceEventIndex > 0 &&
practiceActivities.length > (practiceEventIndex - 1);
bool get isNextEnabled =>
practiceEventIndex >= 0 &&
practiceEventIndex < practiceActivities.length - 1;
@override
void initState() {
super.initState();
loadInitialData();
setCurrentActivity();
}
String? get langCode {
final String? langCode = MatrixState.pangeaController.languageController
.activeL2Model()
?.langCode;
if (langCode == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context)!.noLanguagesSet)),
);
debugger(when: kDebugMode);
return null;
}
return langCode;
}
void loadInitialData() {
if (langCode == null) return;
updatePracticeActivity();
if (practiceEvent == null) {
debugger(when: kDebugMode);
}
}
void updatePracticeActivity() {
if (langCode == null) return;
final List<PracticeActivityEvent> activities =
widget.pangeaMessageEvent.practiceActivities(langCode!);
if (activities.isEmpty) return;
/// Initalizes the current activity.
/// If the current activity hasn't been set yet, show the first
/// uncompleted activity if there is one.
/// If not, show the first activity
void setCurrentActivity() {
if (practiceActivities.isEmpty) return;
final List<PracticeActivityEvent> incompleteActivities =
activities.where((element) => !element.isComplete).toList();
debugPrint("total events: ${activities.length}");
debugPrint("incomplete practice events: ${incompleteActivities.length}");
// TODO update to show next activity
practiceEvent = activities.first;
// // if an incomplete activity is found, show that
// if (incompleteActivities.isNotEmpty) {
// practiceEvent = incompleteActivities.first;
// }
// // if no incomplete activity is found, show the last activity
// else if (activities.isNotEmpty) {
// practiceEvent = activities.last;
// }
practiceActivities.where((element) => !element.isComplete).toList();
currentActivity ??= incompleteActivities.isNotEmpty
? incompleteActivities.first
: practiceActivities.first;
setState(() {});
}
void showNextActivity() {
if (langCode == null) return;
updatePracticeActivity();
widget.controller.updateMode(MessageMode.practiceActivity);
void setCurrentModel(PracticeActivityRecordModel? recordModel) {
currentRecordModel = recordModel;
}
/// Sets the current acitivity based on the given [direction].
void navigateActivities(Direction direction) {
final bool enableNavigation = (direction == Direction.f && isNextEnabled) ||
(direction == Direction.b && isPrevEnabled);
if (enableNavigation) {
currentActivity = practiceActivities[direction == Direction.f
? practiceEventIndex + 1
: practiceEventIndex - 1];
setState(() {});
}
}
/// Sends the current record model and activity to the server.
/// If either the currentRecordModel or currentActivity is null, the method returns early.
/// Sets the [sending] flag to true before sending the record and activity.
/// Logs any errors that occur during the send operation.
/// Sets the [sending] flag to false when the send operation is complete.
void sendRecord() {
if (currentRecordModel == null || currentActivity == null) return;
setState(() => sending = true);
MatrixState.pangeaController.activityRecordController
.send(currentRecordModel!, currentActivity!)
.catchError((error) {
ErrorHandler.logError(
e: error,
s: StackTrace.current,
data: {
'recordModel': currentRecordModel?.toJson(),
'practiceEvent': currentActivity?.event.toJson(),
},
);
return null;
}).whenComplete(() => setState(() => sending = false));
}
@override
Widget build(BuildContext context) {
if (practiceEvent == null) {
final Widget navigationButtons = Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Opacity(
opacity: isPrevEnabled ? 1.0 : 0,
child: IconButton(
onPressed:
isPrevEnabled ? () => navigateActivities(Direction.b) : null,
icon: const Icon(Icons.keyboard_arrow_left_outlined),
tooltip: L10n.of(context)!.previous,
),
),
Expanded(
child: Opacity(
opacity: currentActivity?.userRecord == null ? 1.0 : 0.5,
child: sending
? const CircularProgressIndicator.adaptive()
: TextButton(
onPressed:
currentActivity?.userRecord == null ? sendRecord : null,
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all<Color>(
AppConfig.primaryColor,
),
),
child: Text(L10n.of(context)!.submit),
),
),
),
Opacity(
opacity: isNextEnabled ? 1.0 : 0,
child: IconButton(
onPressed:
isNextEnabled ? () => navigateActivities(Direction.f) : null,
icon: const Icon(Icons.keyboard_arrow_right_outlined),
tooltip: L10n.of(context)!.next,
),
),
],
);
if (currentActivity == null || practiceActivities.isEmpty) {
return Text(
L10n.of(context)!.noActivitiesFound,
style: BotStyle.text(context),
@ -99,10 +159,14 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
// onActivityGenerated: updatePracticeActivity,
// );
}
return PracticeActivityContent(
practiceEvent: practiceEvent!,
pangeaMessageEvent: widget.pangeaMessageEvent,
controller: this,
return Column(
children: [
PracticeActivity(
practiceEvent: currentActivity!,
controller: this,
),
navigationButtons,
],
);
}
}

View file

@ -1,164 +1,43 @@
import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart';
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
class PracticeActivityContent extends StatefulWidget {
/// Practice activity content
class PracticeActivity extends StatefulWidget {
final PracticeActivityEvent practiceEvent;
final PangeaMessageEvent pangeaMessageEvent;
final MessagePracticeActivityCardState controller;
const PracticeActivityContent({
const PracticeActivity({
super.key,
required this.practiceEvent,
required this.pangeaMessageEvent,
required this.controller,
});
@override
MessagePracticeActivityContentState createState() =>
MessagePracticeActivityContentState();
PracticeActivityContentState createState() => PracticeActivityContentState();
}
class MessagePracticeActivityContentState
extends State<PracticeActivityContent> {
int? selectedChoiceIndex;
PracticeActivityRecordModel? recordModel;
bool recordSubmittedThisSession = false;
bool recordSubmittedPreviousSession = false;
PracticeActivityEvent get practiceEvent => widget.practiceEvent;
@override
void initState() {
super.initState();
initalizeActivity();
}
@override
void didUpdateWidget(covariant PracticeActivityContent oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.practiceEvent.event.eventId !=
widget.practiceEvent.event.eventId) {
initalizeActivity();
}
}
void initalizeActivity() {
final PracticeActivityRecordEvent? recordEvent =
widget.practiceEvent.userRecords.firstOrNull;
if (recordEvent?.record == null) {
recordModel = PracticeActivityRecordModel(
question:
widget.practiceEvent.practiceActivity.multipleChoice!.question,
);
} else {
recordModel = recordEvent!.record;
//Note that only MultipleChoice activities will have this so we probably should move this logic to the MultipleChoiceActivity widget
selectedChoiceIndex = recordModel?.latestResponse != null
? widget.practiceEvent.practiceActivity.multipleChoice
?.choiceIndex(recordModel!.latestResponse!)
: null;
recordSubmittedPreviousSession = true;
recordSubmittedThisSession = true;
}
setState(() {});
}
void updateChoice(int index) {
setState(() {
selectedChoiceIndex = index;
recordModel!.addResponse(
text: widget
.practiceEvent.practiceActivity.multipleChoice!.choices[index],
);
});
}
class PracticeActivityContentState extends State<PracticeActivity> {
Widget get activityWidget {
switch (widget.practiceEvent.practiceActivity.activityType) {
case ActivityTypeEnum.multipleChoice:
return MultipleChoiceActivity(
card: this,
updateChoice: updateChoice,
isActive:
!recordSubmittedPreviousSession && !recordSubmittedThisSession,
controller: widget.controller,
currentActivity: widget.practiceEvent,
);
default:
return const SizedBox.shrink();
}
}
void sendRecord() {
MatrixState.pangeaController.activityRecordController
.send(
recordModel!,
widget.practiceEvent,
)
.catchError((error) {
ErrorHandler.logError(
e: error,
s: StackTrace.current,
data: {
'recordModel': recordModel?.toJson(),
'practiceEvent': widget.practiceEvent.event.toJson(),
},
);
return null;
}).then((_) => widget.controller.showNextActivity());
setState(() {
recordSubmittedThisSession = true;
});
}
@override
Widget build(BuildContext context) {
debugPrint(
"MessagePracticeActivityContentState.build with selectedChoiceIndex: $selectedChoiceIndex",
);
return Column(
children: [
activityWidget,
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Opacity(
opacity: selectedChoiceIndex != null &&
!recordSubmittedThisSession &&
!recordSubmittedPreviousSession
? 1.0
: 0.5,
child: TextButton(
onPressed: () {
if (recordSubmittedThisSession ||
recordSubmittedPreviousSession) {
return;
}
selectedChoiceIndex != null ? sendRecord() : null;
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all<Color>(
AppConfig.primaryColor,
),
),
child: Text(L10n.of(context)!.submit),
),
),
],
),
],
);
}

View file

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

View file

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

View file

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

View file

@ -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"))

File diff suppressed because it is too large Load diff

View file

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

View file

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