Merge branch 'main' into cli-sdk-update
This commit is contained in:
commit
e53ba536ca
92 changed files with 1381 additions and 1312 deletions
2
.github/workflows/main_deploy.yaml
vendored
2
.github/workflows/main_deploy.yaml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -13,6 +13,8 @@
|
|||
prime
|
||||
*.env
|
||||
!/public/.env
|
||||
*.env.local_choreo
|
||||
*.env.prod
|
||||
|
||||
# libolm package
|
||||
/assets/js/package
|
||||
|
|
|
|||
|
|
@ -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!",
|
||||
|
|
@ -4065,6 +4065,8 @@
|
|||
"practice": "Practice",
|
||||
"noLanguagesSet": "No languages set",
|
||||
"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": {
|
||||
|
|
@ -4082,5 +4084,6 @@
|
|||
"@interactiveTranslatorAutoPlayDesc": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
}
|
||||
},
|
||||
"changeAnalyticsView": "Change Analytics View"
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -170,31 +170,11 @@ abstract class AppRoutes {
|
|||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const StudentAnalyticsPage(),
|
||||
const StudentAnalyticsPage(
|
||||
selectedView: BarChartViewSelection.messages,
|
||||
),
|
||||
),
|
||||
redirect: loggedOutRedirect,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'messages',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const StudentAnalyticsPage(
|
||||
selectedView: BarChartViewSelection.messages,
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'errors',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const StudentAnalyticsPage(
|
||||
selectedView: BarChartViewSelection.grammar,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: 'analytics',
|
||||
|
|
@ -207,34 +187,13 @@ abstract class AppRoutes {
|
|||
routes: [
|
||||
GoRoute(
|
||||
path: ':spaceid',
|
||||
redirect: loggedOutRedirect,
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const SpaceAnalyticsPage(),
|
||||
const SpaceAnalyticsPage(
|
||||
selectedView: BarChartViewSelection.messages,
|
||||
),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'messages',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const SpaceAnalyticsPage(
|
||||
selectedView: BarChartViewSelection.messages,
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'errors',
|
||||
pageBuilder: (context, state) => defaultPageBuilder(
|
||||
context,
|
||||
state,
|
||||
const SpaceAnalyticsPage(
|
||||
selectedView: BarChartViewSelection.grammar,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import 'package:fluffychat/pages/chat/events/message.dart';
|
|||
import 'package:fluffychat/pages/chat/seen_by_row.dart';
|
||||
import 'package:fluffychat/pages/chat/typing_indicators.dart';
|
||||
import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/utils/instructions.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/locked_chat_message.dart';
|
||||
import 'package:fluffychat/utils/account_config.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
|
|
@ -46,7 +46,7 @@ class ChatEventList extends StatelessWidget {
|
|||
// card, attach it on top of the first shown message
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (events.isEmpty) return;
|
||||
controller.pangeaController.instructions.show(
|
||||
controller.pangeaController.instructions.showInstructionsPopup(
|
||||
context,
|
||||
InstructionsEnum.clickMessage,
|
||||
events[0].eventId,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -89,19 +89,17 @@ class ChatView extends StatelessWidget {
|
|||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
// #Pangea
|
||||
// PopupMenuItem(
|
||||
// value: _EventContextAction.info,
|
||||
// child: Row(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// const Icon(Icons.info_outlined),
|
||||
// const SizedBox(width: 12),
|
||||
// Text(L10n.of(context)!.messageInfo),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// Pangea#
|
||||
PopupMenuItem(
|
||||
value: _EventContextAction.info,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.info_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context)!.messageInfo),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (controller.selectedEvents.single.status.isSent)
|
||||
PopupMenuItem(
|
||||
value: _EventContextAction.report,
|
||||
|
|
@ -442,7 +440,8 @@ class ChatView extends StatelessWidget {
|
|||
children: [
|
||||
const ConnectionStatusHeader(),
|
||||
ITBar(
|
||||
choreographer: controller.choreographer,
|
||||
choreographer:
|
||||
controller.choreographer,
|
||||
),
|
||||
ReactionsPicker(controller),
|
||||
ReplyDisplay(controller),
|
||||
|
|
|
|||
|
|
@ -470,7 +470,7 @@ class Message extends StatelessWidget {
|
|||
?.showUseType ??
|
||||
false) ...[
|
||||
pangeaMessageEvent!
|
||||
.useType
|
||||
.msgUseType
|
||||
.iconView(
|
||||
context,
|
||||
textColor
|
||||
|
|
|
|||
|
|
@ -917,7 +917,7 @@ class ChatListController extends State<ChatList>
|
|||
if (mounted) {
|
||||
GoogleAnalytics.analyticsUserUpdate(client.userID);
|
||||
await pangeaController.subscriptionController.initialize();
|
||||
await pangeaController.myAnalytics.addEventsListener();
|
||||
await pangeaController.myAnalytics.initialize();
|
||||
pangeaController.afterSyncAndFirstLoginInitialization(context);
|
||||
await pangeaController.inviteBotToExistingSpaces();
|
||||
await pangeaController.setPangeaPushRules();
|
||||
|
|
|
|||
|
|
@ -5,13 +5,12 @@ import 'package:fluffychat/pages/chat/chat.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/controllers/alternative_translator.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/message_options.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/assistance_state_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/edit_type.dart';
|
||||
import 'package:fluffychat/pangea/models/it_step.dart';
|
||||
import 'package:fluffychat/pangea/models/language_detection_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
|
|
@ -25,7 +24,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../../widgets/matrix.dart';
|
||||
import '../../enum/use_type.dart';
|
||||
import '../../models/choreo_record.dart';
|
||||
import '../../models/language_model.dart';
|
||||
import '../../models/pangea_match_model.dart';
|
||||
|
|
@ -95,63 +93,59 @@ class Choreographer {
|
|||
}
|
||||
|
||||
Future<void> _sendWithIGC(BuildContext context) async {
|
||||
if (igc.canSendMessage) {
|
||||
final PangeaRepresentation? originalWritten =
|
||||
choreoRecord.includedIT && itController.sourceText != null
|
||||
? PangeaRepresentation(
|
||||
langCode: l1LangCode ?? LanguageKeys.unknownLanguage,
|
||||
text: itController.sourceText!,
|
||||
originalWritten: true,
|
||||
originalSent: false,
|
||||
)
|
||||
: null;
|
||||
|
||||
final PangeaRepresentation originalSent = PangeaRepresentation(
|
||||
langCode: langCodeOfCurrentText ?? LanguageKeys.unknownLanguage,
|
||||
text: currentText,
|
||||
originalSent: true,
|
||||
originalWritten: originalWritten == null,
|
||||
);
|
||||
final ChoreoRecord? applicableChoreo =
|
||||
isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null;
|
||||
|
||||
// if the message has not been processed to determine its language
|
||||
// then run it through the language detection endpoint. If the detection
|
||||
// confidence is high enough, use that language code as the message's language
|
||||
// to save that pangea representation
|
||||
if (applicableChoreo == null) {
|
||||
final resp = await pangeaController.languageDetection.detectLanguage(
|
||||
currentText,
|
||||
pangeaController.languageController.userL2?.langCode,
|
||||
pangeaController.languageController.userL1?.langCode,
|
||||
);
|
||||
final LanguageDetection? bestDetection = resp.bestDetection();
|
||||
if (bestDetection != null) {
|
||||
originalSent.langCode = bestDetection.langCode;
|
||||
}
|
||||
}
|
||||
|
||||
final UseType useType = useTypeCalculator(applicableChoreo);
|
||||
debugPrint("use type in choreographer $useType");
|
||||
|
||||
chatController.send(
|
||||
// PTODO - turn this back on in conjunction with saving tokens
|
||||
// we need to save those tokens as well, in order for exchanges to work
|
||||
// properly. in an exchange, the other user will want
|
||||
// originalWritten: originalWritten,
|
||||
originalSent: originalSent,
|
||||
tokensSent: igc.igcTextData?.tokens != null
|
||||
? PangeaMessageTokens(tokens: igc.igcTextData!.tokens)
|
||||
: null,
|
||||
//TODO - save originalwritten tokens
|
||||
choreo: applicableChoreo,
|
||||
useType: useType,
|
||||
);
|
||||
|
||||
clear();
|
||||
} else {
|
||||
if (!igc.canSendMessage) {
|
||||
igc.showFirstMatch(context);
|
||||
return;
|
||||
}
|
||||
|
||||
final PangeaRepresentation? originalWritten =
|
||||
choreoRecord.includedIT && itController.sourceText != null
|
||||
? PangeaRepresentation(
|
||||
langCode: l1LangCode ?? LanguageKeys.unknownLanguage,
|
||||
text: itController.sourceText!,
|
||||
originalWritten: true,
|
||||
originalSent: false,
|
||||
)
|
||||
: null;
|
||||
|
||||
// TODO - why does both it and igc need to be enabled for choreo to be applicable?
|
||||
// final ChoreoRecord? applicableChoreo =
|
||||
// isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null;
|
||||
|
||||
// if tokens or language detection are not available, we should get them
|
||||
// notes
|
||||
// 1) we probably need to move this to after we clear the input field
|
||||
// or the user could experience some lag here.
|
||||
// 2) that this call is being made after we've determined if we have an applicable choreo in order to
|
||||
// say whether correction was run on the message. we may eventually want
|
||||
// to edit the useType after
|
||||
if (igc.igcTextData?.tokens == null ||
|
||||
igc.igcTextData?.detectedLanguage == null) {
|
||||
await igc.getIGCTextData(onlyTokensAndLanguageDetection: true);
|
||||
}
|
||||
|
||||
final PangeaRepresentation originalSent = PangeaRepresentation(
|
||||
langCode:
|
||||
igc.igcTextData?.detectedLanguage ?? LanguageKeys.unknownLanguage,
|
||||
text: currentText,
|
||||
originalSent: true,
|
||||
originalWritten: originalWritten == null,
|
||||
);
|
||||
|
||||
final PangeaMessageTokens? tokensSent = igc.igcTextData?.tokens != null
|
||||
? PangeaMessageTokens(tokens: igc.igcTextData!.tokens)
|
||||
: null;
|
||||
|
||||
chatController.send(
|
||||
// originalWritten: originalWritten,
|
||||
originalSent: originalSent,
|
||||
tokensSent: tokensSent,
|
||||
//TODO - save originalwritten tokens
|
||||
// choreo: applicableChoreo,
|
||||
choreo: choreoRecord,
|
||||
);
|
||||
|
||||
clear();
|
||||
}
|
||||
|
||||
_resetDebounceTimer() {
|
||||
|
|
@ -167,7 +161,7 @@ class Choreographer {
|
|||
}
|
||||
choreoMode = ChoreoMode.it;
|
||||
itController.initializeIT(
|
||||
ITStartData(_textController.text, igc.detectedLangCode),
|
||||
ITStartData(_textController.text, igc.igcTextData?.detectedLanguage),
|
||||
);
|
||||
itMatch.status = PangeaMatchStatus.accepted;
|
||||
|
||||
|
|
@ -180,6 +174,7 @@ class Choreographer {
|
|||
_textController.setSystemText("", EditType.itStart);
|
||||
}
|
||||
|
||||
/// Handles any changes to the text input
|
||||
_onChangeListener() {
|
||||
if (_noChange) {
|
||||
return;
|
||||
|
|
@ -188,21 +183,26 @@ class Choreographer {
|
|||
if ([
|
||||
EditType.igc,
|
||||
].contains(_textController.editType)) {
|
||||
// this may be unnecessary now that tokens are not used
|
||||
// to allow click of words in the input field and we're getting this at the end
|
||||
// TODO - turn it off and tested that this is fine
|
||||
igc.justGetTokensAndAddThemToIGCTextData();
|
||||
|
||||
// we set editType to keyboard here because that is the default for it
|
||||
// and we want to make sure that the next change is treated as a keyboard change
|
||||
// unless the system explicity sets it to something else. this
|
||||
textController.editType = EditType.keyboard;
|
||||
return;
|
||||
}
|
||||
|
||||
// not sure if this is necessary now
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
|
||||
if (errorService.isError) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if (igc.igcTextData != null) {
|
||||
igc.clear();
|
||||
// setState();
|
||||
// }
|
||||
|
||||
_resetDebounceTimer();
|
||||
|
||||
|
|
@ -212,7 +212,9 @@ class Choreographer {
|
|||
() => getLanguageHelp(),
|
||||
);
|
||||
} else {
|
||||
getLanguageHelp(ChoreoMode.it == choreoMode);
|
||||
getLanguageHelp(
|
||||
onlyTokensAndLanguageDetection: ChoreoMode.it == choreoMode,
|
||||
);
|
||||
}
|
||||
|
||||
//Note: we don't set the keyboard type on each keyboard stroke so this is how we default to
|
||||
|
|
@ -221,10 +223,14 @@ class Choreographer {
|
|||
textController.editType = EditType.keyboard;
|
||||
}
|
||||
|
||||
Future<void> getLanguageHelp([
|
||||
bool tokensOnly = false,
|
||||
/// Fetches the language help for the current text, including grammar correction, language detection,
|
||||
/// tokens, and translations. Includes logic to exit the flow if the user is not subscribed, if the tools are not enabled, or
|
||||
/// or if autoIGC is not enabled and the user has not manually requested it.
|
||||
/// [onlyTokensAndLanguageDetection] will
|
||||
Future<void> getLanguageHelp({
|
||||
bool onlyTokensAndLanguageDetection = false,
|
||||
bool manual = false,
|
||||
]) async {
|
||||
}) async {
|
||||
try {
|
||||
if (errorService.isError) return;
|
||||
final CanSendStatus canSendStatus =
|
||||
|
|
@ -239,13 +245,15 @@ class Choreographer {
|
|||
startLoading();
|
||||
if (choreoMode == ChoreoMode.it &&
|
||||
itController.isTranslationDone &&
|
||||
!tokensOnly) {
|
||||
!onlyTokensAndLanguageDetection) {
|
||||
// debugger(when: kDebugMode);
|
||||
}
|
||||
|
||||
await (choreoMode == ChoreoMode.it && !itController.isTranslationDone
|
||||
? itController.getTranslationData(_useCustomInput)
|
||||
: igc.getIGCTextData(tokensOnly: tokensOnly));
|
||||
: igc.getIGCTextData(
|
||||
onlyTokensAndLanguageDetection: onlyTokensAndLanguageDetection,
|
||||
));
|
||||
} catch (err, stack) {
|
||||
ErrorHandler.logError(e: err, s: stack);
|
||||
} finally {
|
||||
|
|
@ -482,14 +490,6 @@ class Choreographer {
|
|||
|
||||
bool get editTypeIsKeyboard => EditType.keyboard == _textController.editType;
|
||||
|
||||
String? get langCodeOfCurrentText {
|
||||
if (igc.detectedLangCode != null) return igc.detectedLangCode!;
|
||||
|
||||
if (itController.isOpen) return l2LangCode!;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
setState() {
|
||||
if (!stateListener.isClosed) {
|
||||
stateListener.add(0);
|
||||
|
|
@ -523,9 +523,11 @@ class Choreographer {
|
|||
chatController.room,
|
||||
);
|
||||
|
||||
bool get itAutoPlayEnabled => pangeaController.pStoreService.read(
|
||||
bool get itAutoPlayEnabled =>
|
||||
pangeaController.pStoreService.read(
|
||||
MatrixProfile.itAutoPlay.title,
|
||||
) ?? false;
|
||||
) ??
|
||||
false;
|
||||
|
||||
bool get definitionsEnabled =>
|
||||
pangeaController.permissionsController.isToolEnabled(
|
||||
|
|
|
|||
|
|
@ -3,18 +3,17 @@ import 'dart:developer';
|
|||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/controllers/span_data_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/span_data_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/igc_text_data_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/igc_repo.dart';
|
||||
import 'package:fluffychat/pangea/repo/tokens_repo.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/span_card.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../models/language_detection_model.dart';
|
||||
import '../../models/span_card_model.dart';
|
||||
import '../../repo/tokens_repo.dart';
|
||||
import '../../utils/error_handler.dart';
|
||||
import '../../utils/overlay.dart';
|
||||
|
||||
|
|
@ -29,59 +28,42 @@ class IgcController {
|
|||
spanDataController = SpanDataController(choreographer);
|
||||
}
|
||||
|
||||
Future<void> getIGCTextData({required bool tokensOnly}) async {
|
||||
Future<void> getIGCTextData({
|
||||
required bool onlyTokensAndLanguageDetection,
|
||||
}) async {
|
||||
try {
|
||||
if (choreographer.currentText.isEmpty) return clear();
|
||||
|
||||
// the error spans are going to be reloaded, so clear the cache
|
||||
spanDataController.clearCache();
|
||||
debugPrint('getIGCTextData called with ${choreographer.currentText}');
|
||||
debugPrint('getIGCTextData called with tokensOnly = $tokensOnly');
|
||||
debugPrint(
|
||||
'getIGCTextData called with tokensOnly = $onlyTokensAndLanguageDetection',
|
||||
);
|
||||
|
||||
final IGCRequestBody reqBody = IGCRequestBody(
|
||||
fullText: choreographer.currentText,
|
||||
userL1: choreographer.l1LangCode!,
|
||||
userL2: choreographer.l2LangCode!,
|
||||
enableIGC: choreographer.igcEnabled && !tokensOnly,
|
||||
enableIT: choreographer.itEnabled && !tokensOnly,
|
||||
tokensOnly: tokensOnly,
|
||||
enableIGC: choreographer.igcEnabled && !onlyTokensAndLanguageDetection,
|
||||
enableIT: choreographer.itEnabled && !onlyTokensAndLanguageDetection,
|
||||
);
|
||||
|
||||
final IGCTextData igcTextDataResponse = await IgcRepo.getIGC(
|
||||
await choreographer.accessToken,
|
||||
igcRequest: reqBody,
|
||||
);
|
||||
// temp fix
|
||||
igcTextDataResponse.originalInput = reqBody.fullText;
|
||||
|
||||
//this will happen when the user changes the input while igc is fetching results
|
||||
// this will happen when the user changes the input while igc is fetching results
|
||||
if (igcTextDataResponse.originalInput != choreographer.currentText) {
|
||||
// final current = choreographer.currentText;
|
||||
// final igctext = igcTextDataResponse.originalInput;
|
||||
// Sentry.addBreadcrumb(
|
||||
// Breadcrumb(message: "igc return input does not match current text"),
|
||||
// );
|
||||
// debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
//TO-DO: in api call, specify turning off IT and/or grammar checking
|
||||
if (!choreographer.igcEnabled) {
|
||||
igcTextDataResponse.matches = igcTextDataResponse.matches
|
||||
.where((match) => !match.isGrammarMatch)
|
||||
.toList();
|
||||
}
|
||||
if (!choreographer.itEnabled) {
|
||||
igcTextDataResponse.matches = igcTextDataResponse.matches
|
||||
.where((match) => !match.isOutOfTargetMatch)
|
||||
.toList();
|
||||
}
|
||||
if (!choreographer.itEnabled && !choreographer.igcEnabled) {
|
||||
igcTextDataResponse.matches = [];
|
||||
}
|
||||
|
||||
igcTextData = igcTextDataResponse;
|
||||
|
||||
// TODO - for each new match,
|
||||
// check if existing igcTextData has one and only one match with the same error text and correction
|
||||
// if so, keep the original match and discard the new one
|
||||
// if not, add the new match to the existing igcTextData
|
||||
|
||||
// After fetching igc data, pre-call span details for each match optimistically.
|
||||
// This will make the loading of span details faster for the user
|
||||
if (igcTextData?.matches.isNotEmpty ?? false) {
|
||||
|
|
@ -170,11 +152,9 @@ class IgcController {
|
|||
const int firstMatchIndex = 0;
|
||||
final PangeaMatch match = igcTextData!.matches[firstMatchIndex];
|
||||
|
||||
if (
|
||||
match.isITStart &&
|
||||
if (match.isITStart &&
|
||||
choreographer.itAutoPlayEnabled &&
|
||||
igcTextData != null
|
||||
) {
|
||||
igcTextData != null) {
|
||||
choreographer.onITStart(igcTextData!.matches[firstMatchIndex]);
|
||||
return;
|
||||
}
|
||||
|
|
@ -215,14 +195,6 @@ class IgcController {
|
|||
return true;
|
||||
}
|
||||
|
||||
String? get detectedLangCode {
|
||||
if (!hasRelevantIGCTextData) return null;
|
||||
|
||||
final LanguageDetection first = igcTextData!.detections.first;
|
||||
|
||||
return first.langCode;
|
||||
}
|
||||
|
||||
clear() {
|
||||
igcTextData = null;
|
||||
spanDataController.clearCache();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'dart:developer';
|
|||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -72,7 +71,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(
|
||||
|
|
@ -86,32 +84,23 @@ class ITController {
|
|||
throw Exception("null _itStartData or empty text in _setSourceText");
|
||||
}
|
||||
debugPrint("_setSourceText with detectedLang ${_itStartData!.langCode}");
|
||||
if (_itStartData!.langCode == choreographer.l1LangCode) {
|
||||
sourceText = _itStartData!.text;
|
||||
return;
|
||||
}
|
||||
|
||||
final FullTextTranslationResponseModel res =
|
||||
await FullTextTranslationRepo.translate(
|
||||
accessToken: await choreographer.accessToken,
|
||||
request: FullTextTranslationRequestModel(
|
||||
text: _itStartData!.text,
|
||||
tgtLang: choreographer.l1LangCode!,
|
||||
srcLang: choreographer.l2LangCode,
|
||||
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;
|
||||
// }
|
||||
// if (_itStartData!.langCode == choreographer.l1LangCode) {
|
||||
sourceText = _itStartData!.text;
|
||||
return;
|
||||
// }
|
||||
|
||||
// final FullTextTranslationResponseModel res =
|
||||
// await FullTextTranslationRepo.translate(
|
||||
// accessToken: await choreographer.accessToken,
|
||||
// request: FullTextTranslationRequestModel(
|
||||
// text: _itStartData!.text,
|
||||
// tgtLang: choreographer.l1LangCode!,
|
||||
// srcLang: _itStartData!.langCode,
|
||||
// userL1: choreographer.l1LangCode!,
|
||||
// userL2: choreographer.l2LangCode!,
|
||||
// ),
|
||||
// );
|
||||
// sourceText = res.bestTranslation;
|
||||
}
|
||||
|
||||
// used 1) at very beginning (with custom input = null)
|
||||
|
|
@ -167,7 +156,7 @@ class ITController {
|
|||
|
||||
if (isTranslationDone) {
|
||||
choreographer.altTranslator.setTranslationFeedback();
|
||||
choreographer.getLanguageHelp(true);
|
||||
choreographer.getLanguageHelp(onlyTokensAndLanguageDetection: true);
|
||||
} else {
|
||||
getNextTranslationData();
|
||||
}
|
||||
|
|
@ -218,7 +207,6 @@ class ITController {
|
|||
|
||||
Future<void> onEditSourceTextSubmit(String newSourceText) async {
|
||||
try {
|
||||
|
||||
_isOpen = true;
|
||||
_isEditingSourceText = false;
|
||||
_itStartData = ITStartData(newSourceText, choreographer.l1LangCode);
|
||||
|
|
@ -230,7 +218,6 @@ class ITController {
|
|||
|
||||
_setSourceText();
|
||||
getTranslationData(false);
|
||||
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
if (err is! http.Response) {
|
||||
|
|
@ -332,9 +319,6 @@ class ITController {
|
|||
|
||||
bool get isLoading => choreographer.isFetching;
|
||||
|
||||
bool get correctChoicesSelected =>
|
||||
completedITSteps.every((ITStep step) => step.isCorrect);
|
||||
|
||||
String latestChoiceFeedback(BuildContext context) =>
|
||||
completedITSteps.isNotEmpty
|
||||
? completedITSteps.last.choiceFeedback(context)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/utils/instructions.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../widgets/common/bot_face_svg.dart';
|
||||
import '../controllers/choreographer.dart';
|
||||
import '../controllers/it_controller.dart';
|
||||
|
|
@ -37,7 +37,7 @@ class ITBotButton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
choreographer.pangeaController.instructions.show(
|
||||
choreographer.pangeaController.instructions.showInstructionsPopup(
|
||||
context,
|
||||
InstructionsEnum.itInstructions,
|
||||
choreographer.itBotTransformTargetKey,
|
||||
|
|
@ -46,7 +46,8 @@ class ITBotButton extends StatelessWidget {
|
|||
|
||||
return IconButton(
|
||||
icon: const BotFace(width: 40.0, expression: BotExpression.idle),
|
||||
onPressed: () => choreographer.pangeaController.instructions.show(
|
||||
onPressed: () =>
|
||||
choreographer.pangeaController.instructions.showInstructionsPopup(
|
||||
context,
|
||||
InstructionsEnum.itInstructions,
|
||||
choreographer.itBotTransformTargetKey,
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
class PrefKey {
|
||||
static const lastFetched = 'LAST_FETCHED';
|
||||
static const flags = 'flags';
|
||||
}
|
||||
24
lib/pangea/constants/language_constants.dart
Normal file
24
lib/pangea/constants/language_constants.dart
Normal 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;
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
class LanguageKeys {
|
||||
static const unknownLanguage = "unk";
|
||||
static const mixedLanguage = "mixed";
|
||||
static const defaultLanguage = "en";
|
||||
static const multiLanguage = "multi";
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
class LanguageLevelType {
|
||||
static List<int> get allInts => [0, 1, 2, 3, 4, 5, 6];
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
class PrefKey {
|
||||
static const lastFetched = 'p_lang_lastfetched';
|
||||
static const flags = 'p_lang_flag';
|
||||
}
|
||||
|
|
@ -66,7 +66,6 @@ class ModelKey {
|
|||
static const String tokensSent = "tokens_sent";
|
||||
static const String tokensWritten = "tokens_written";
|
||||
static const String choreoRecord = "choreo_record";
|
||||
static const String useType = "use_type";
|
||||
|
||||
static const String baseDefinition = "base_definition";
|
||||
static const String targetDefinition = "target_definition";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/language_detection_model.dart';
|
||||
import 'package:fluffychat/pangea/network/urls.dart';
|
||||
|
|
@ -75,19 +76,21 @@ class LanguageDetectionResponse {
|
|||
};
|
||||
}
|
||||
|
||||
LanguageDetection? get _bestDetection {
|
||||
/// Return the highest confidence detection.
|
||||
/// If there are no detections, the unknown language detection is returned.
|
||||
LanguageDetection get highestConfidenceDetection {
|
||||
detections.sort((a, b) => b.confidence.compareTo(a.confidence));
|
||||
return detections.isNotEmpty ? detections.first : null;
|
||||
return detections.firstOrNull ?? unknownLanguageDetection;
|
||||
}
|
||||
|
||||
final double _confidenceThreshold = 0.95;
|
||||
|
||||
LanguageDetection? bestDetection({double? threshold}) {
|
||||
threshold ??= _confidenceThreshold;
|
||||
return (_bestDetection?.confidence ?? 0) >= _confidenceThreshold
|
||||
? _bestDetection!
|
||||
: null;
|
||||
}
|
||||
/// Returns the highest validated detection based on the confidence threshold.
|
||||
/// If the highest confidence detection is below the threshold, the unknown language
|
||||
/// detection is returned.
|
||||
LanguageDetection highestValidatedDetection({double? threshold}) =>
|
||||
highestConfidenceDetection.confidence >=
|
||||
(threshold ?? languageDetectionConfidenceThreshold)
|
||||
? highestConfidenceDetection
|
||||
: unknownLanguageDetection;
|
||||
}
|
||||
|
||||
class _LanguageDetectionCacheItem {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/language_repo.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../constants/language_list_keys.dart';
|
||||
import '../utils/shared_prefs.dart';
|
||||
|
||||
class PangeaLanguage {
|
||||
|
|
|
|||
|
|
@ -641,7 +641,7 @@ class AnalyticsController extends BaseController {
|
|||
|
||||
List<ConstructAnalyticsEvent>? getConstructsLocal({
|
||||
required TimeSpan timeSpan,
|
||||
required ConstructType constructType,
|
||||
required ConstructTypeEnum constructType,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
DateTime? lastUpdated,
|
||||
|
|
@ -669,7 +669,7 @@ class AnalyticsController extends BaseController {
|
|||
}
|
||||
|
||||
void cacheConstructs({
|
||||
required ConstructType constructType,
|
||||
required ConstructTypeEnum constructType,
|
||||
required List<ConstructAnalyticsEvent> events,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
|
|
@ -687,7 +687,7 @@ class AnalyticsController extends BaseController {
|
|||
|
||||
Future<List<ConstructAnalyticsEvent>> getMyConstructs({
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required ConstructType constructType,
|
||||
required ConstructTypeEnum constructType,
|
||||
AnalyticsSelected? selected,
|
||||
}) async {
|
||||
final List<ConstructAnalyticsEvent> unfilteredConstructs =
|
||||
|
|
@ -706,7 +706,7 @@ class AnalyticsController extends BaseController {
|
|||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> getSpaceConstructs({
|
||||
required ConstructType constructType,
|
||||
required ConstructTypeEnum constructType,
|
||||
required Room space,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
|
|
@ -768,7 +768,7 @@ class AnalyticsController extends BaseController {
|
|||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>?> getConstructs({
|
||||
required ConstructType constructType,
|
||||
required ConstructTypeEnum constructType,
|
||||
required AnalyticsSelected defaultSelected,
|
||||
AnalyticsSelected? selected,
|
||||
bool removeIT = true,
|
||||
|
|
@ -898,7 +898,7 @@ abstract class CacheEntry {
|
|||
}
|
||||
|
||||
class ConstructCacheEntry extends CacheEntry {
|
||||
final ConstructType type;
|
||||
final ConstructTypeEnum type;
|
||||
final List<ConstructAnalyticsEvent> events;
|
||||
|
||||
ConstructCacheEntry({
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/base_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -18,11 +15,18 @@ import 'package:matrix/matrix.dart';
|
|||
import '../extensions/client_extension/client_extension.dart';
|
||||
import '../extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
|
||||
// controls the sending of analytics events
|
||||
class MyAnalyticsController extends BaseController {
|
||||
/// handles the processing of analytics for
|
||||
/// 1) messages sent by the user and
|
||||
/// 2) constructs used by the user, both in sending messages and doing practice activities
|
||||
class MyAnalyticsController {
|
||||
late PangeaController _pangeaController;
|
||||
Timer? _updateTimer;
|
||||
|
||||
/// the max number of messages that will be cached before
|
||||
/// an automatic update is triggered
|
||||
final int _maxMessagesCached = 10;
|
||||
|
||||
/// the number of minutes before an automatic update is triggered
|
||||
final int _minutesBeforeUpdate = 5;
|
||||
|
||||
/// the time since the last update that will trigger an automatic update
|
||||
|
|
@ -33,41 +37,50 @@ class MyAnalyticsController extends BaseController {
|
|||
}
|
||||
|
||||
/// adds the listener that handles when to run automatic updates
|
||||
/// to analytics - either after a certain number of messages sent/
|
||||
/// to analytics - either after a certain number of messages sent
|
||||
/// received or after a certain amount of time [_timeSinceUpdate] without an update
|
||||
Future<void> addEventsListener() async {
|
||||
final Client client = _pangeaController.matrixState.client;
|
||||
Future<void> initialize() async {
|
||||
final lastUpdated = await _refreshAnalyticsIfOutdated();
|
||||
|
||||
// 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,192 +190,135 @@ class MyAnalyticsController extends BaseController {
|
|||
}
|
||||
}
|
||||
|
||||
// top level analytics sending function. Send analytics
|
||||
// for each type of analytics event
|
||||
// to each of the applicable analytics rooms
|
||||
String? get userL2 => _pangeaController.languageController.activeL2Code();
|
||||
|
||||
/// top level analytics sending function. Gather recent messages and activity records,
|
||||
/// convert them into the correct formats, and send them to the analytics room
|
||||
Future<void> _updateAnalytics() async {
|
||||
// if the user's l2 is not sent, don't send analytics
|
||||
final String? userL2 = _pangeaController.languageController.activeL2Code();
|
||||
if (userL2 == null) {
|
||||
// if missing important info, don't send analytics
|
||||
if (userL2 == null || _client.userID == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
// fetch a list of all the chats that the user is studying
|
||||
// and a list of all the spaces in which the user is studying
|
||||
await setStudentChats();
|
||||
await setStudentSpaces();
|
||||
// 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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ class PracticeGenerationController {
|
|||
PracticeActivityModel dummyModel(PangeaMessageEvent event) =>
|
||||
PracticeActivityModel(
|
||||
tgtConstructs: [
|
||||
ConstructIdentifier(lemma: "be", type: ConstructType.vocab),
|
||||
ConstructIdentifier(lemma: "be", type: ConstructTypeEnum.vocab),
|
||||
],
|
||||
activityType: ActivityTypeEnum.multipleChoice,
|
||||
langCode: event.messageDisplayLangCode,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/controllers/base_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/repo/word_repo.dart';
|
||||
import '../models/word_data_model.dart';
|
||||
import 'base_controller.dart';
|
||||
|
|
|
|||
|
|
@ -29,15 +29,4 @@ extension BarChartViewSelectionExtension on BarChartViewSelection {
|
|||
return Icons.spellcheck_outlined;
|
||||
}
|
||||
}
|
||||
|
||||
String get route {
|
||||
switch (this) {
|
||||
case BarChartViewSelection.messages:
|
||||
return 'messages';
|
||||
// case BarChartViewSelection.vocab:
|
||||
// return 'vocab';
|
||||
case BarChartViewSelection.grammar:
|
||||
return 'errors';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
93
lib/pangea/enum/construct_use_type_enum.dart
Normal file
93
lib/pangea/enum/construct_use_type_enum.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
45
lib/pangea/enum/instructions_enum.dart
Normal file
45
lib/pangea/enum/instructions_enum.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
enum InstructionsEnum {
|
||||
itInstructions,
|
||||
clickMessage,
|
||||
blurMeansTranslate,
|
||||
tooltipInstructions,
|
||||
speechToText,
|
||||
}
|
||||
|
||||
extension Copy on InstructionsEnum {
|
||||
String title(BuildContext context) {
|
||||
switch (this) {
|
||||
case InstructionsEnum.itInstructions:
|
||||
return L10n.of(context)!.itInstructionsTitle;
|
||||
case InstructionsEnum.clickMessage:
|
||||
return L10n.of(context)!.clickMessageTitle;
|
||||
case InstructionsEnum.blurMeansTranslate:
|
||||
return L10n.of(context)!.blurMeansTranslateTitle;
|
||||
case InstructionsEnum.tooltipInstructions:
|
||||
return L10n.of(context)!.tooltipInstructionsTitle;
|
||||
case InstructionsEnum.speechToText:
|
||||
return L10n.of(context)!.hintTitle;
|
||||
}
|
||||
}
|
||||
|
||||
String body(BuildContext context) {
|
||||
switch (this) {
|
||||
case InstructionsEnum.itInstructions:
|
||||
return L10n.of(context)!.itInstructionsBody;
|
||||
case InstructionsEnum.clickMessage:
|
||||
return L10n.of(context)!.clickMessageBody;
|
||||
case InstructionsEnum.blurMeansTranslate:
|
||||
return L10n.of(context)!.blurMeansTranslateBody;
|
||||
case InstructionsEnum.speechToText:
|
||||
return L10n.of(context)!.speechToTextBody;
|
||||
case InstructionsEnum.tooltipInstructions:
|
||||
return PlatformInfos.isMobile
|
||||
? L10n.of(context)!.tooltipInstructionsMobileBody
|
||||
: L10n.of(context)!.tooltipInstructionsBrowserBody;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../models/choreo_record.dart';
|
||||
import '../utils/bot_style.dart';
|
||||
|
||||
enum UseType { wa, ta, ga, un }
|
||||
|
|
@ -93,17 +91,3 @@ extension UseTypeMethods on UseType {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
UseType useTypeCalculator(
|
||||
ChoreoRecord? choreoRecord,
|
||||
) {
|
||||
if (choreoRecord == null) {
|
||||
return UseType.un;
|
||||
} else if (choreoRecord.includedIT) {
|
||||
return UseType.ta;
|
||||
} else if (choreoRecord.hasAcceptedMatches) {
|
||||
return UseType.ga;
|
||||
} else {
|
||||
return UseType.wa;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,9 @@ extension PangeaClient on Client {
|
|||
|
||||
Future<List<Room>> get spacesImTeaching async => await _spacesImTeaching;
|
||||
|
||||
Future<List<Room>> get spacesImStudyingIn async => await _spacesImStudyingIn;
|
||||
Future<List<Room>> get chatsImAStudentIn async => await _chatsImAStudentIn;
|
||||
|
||||
Future<List<Room>> get spaceImAStudentIn async => await _spacesImStudyingIn;
|
||||
|
||||
List<Room> get spacesImIn => _spacesImIn;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,18 @@ extension SpaceClientExtension on Client {
|
|||
return spaces;
|
||||
}
|
||||
|
||||
Future<List<Room>> get _chatsImAStudentIn async {
|
||||
final List<String> nowteacherRoomIds = await teacherRoomIds;
|
||||
return rooms
|
||||
.where(
|
||||
(r) =>
|
||||
!r.isSpace &&
|
||||
!r.isAnalyticsRoom &&
|
||||
!nowteacherRoomIds.contains(r.id),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<List<Room>> get _spacesImStudyingIn async {
|
||||
final List<Room> joinedSpaces = rooms
|
||||
.where(
|
||||
|
|
|
|||
|
|
@ -229,7 +229,6 @@ extension EventsRoomExtension on Room {
|
|||
PangeaMessageTokens? tokensSent,
|
||||
PangeaMessageTokens? tokensWritten,
|
||||
ChoreoRecord? choreo,
|
||||
UseType? useType,
|
||||
}) {
|
||||
// if (parseCommands) {
|
||||
// return client.parseAndRunCommand(this, message,
|
||||
|
|
@ -247,7 +246,6 @@ extension EventsRoomExtension on Room {
|
|||
ModelKey.originalWritten: originalWritten?.toJson(),
|
||||
ModelKey.tokensSent: tokensSent?.toJson(),
|
||||
ModelKey.tokensWritten: tokensWritten?.toJson(),
|
||||
ModelKey.useType: useType?.string,
|
||||
};
|
||||
if (parseMarkdown) {
|
||||
final html = markdown(
|
||||
|
|
@ -347,7 +345,7 @@ extension EventsRoomExtension on Room {
|
|||
RecentMessageRecord(
|
||||
eventId: event.eventId,
|
||||
chatId: id,
|
||||
useType: pMsgEvent.useType,
|
||||
useType: pMsgEvent.msgUseType,
|
||||
time: event.originServerTs,
|
||||
),
|
||||
);
|
||||
|
|
@ -426,26 +424,6 @@ extension EventsRoomExtension on Room {
|
|||
// }
|
||||
// }
|
||||
|
||||
Future<List<PangeaMessageEvent>> myMessageEventsInChat({
|
||||
DateTime? since,
|
||||
}) async {
|
||||
final List<Event> msgEvents = await getEventsBySender(
|
||||
type: EventTypes.Message,
|
||||
sender: client.userID!,
|
||||
since: since,
|
||||
);
|
||||
final Timeline timeline = await getTimeline();
|
||||
return msgEvents
|
||||
.where((event) => (event.content['msgtype'] == MessageTypes.Text))
|
||||
.map((event) {
|
||||
return PangeaMessageEvent(
|
||||
event: event,
|
||||
timeline: timeline,
|
||||
ownMessage: true,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// fetch event of a certain type by a certain sender
|
||||
// since a certain time or up to a certain amount
|
||||
Future<List<Event>> getEventsBySender({
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import 'dart:developer';
|
|||
import 'package:adaptive_dialog/adaptive_dialog.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_room_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
|
|
@ -33,7 +34,6 @@ import 'package:sentry_flutter/sentry_flutter.dart';
|
|||
|
||||
import '../../../config/app_config.dart';
|
||||
import '../../constants/pangea_event_types.dart';
|
||||
import '../../enum/use_type.dart';
|
||||
import '../../models/choreo_record.dart';
|
||||
import '../../models/representation_content_model.dart';
|
||||
import '../client_extension/client_extension.dart';
|
||||
|
|
@ -180,7 +180,6 @@ extension PangeaRoom on Room {
|
|||
PangeaMessageTokens? tokensSent,
|
||||
PangeaMessageTokens? tokensWritten,
|
||||
ChoreoRecord? choreo,
|
||||
UseType? useType,
|
||||
}) =>
|
||||
_pangeaSendTextEvent(
|
||||
message,
|
||||
|
|
@ -197,7 +196,6 @@ extension PangeaRoom on Room {
|
|||
tokensSent: tokensSent,
|
||||
tokensWritten: tokensWritten,
|
||||
choreo: choreo,
|
||||
useType: useType,
|
||||
);
|
||||
|
||||
Future<String> updateStateEvent(Event stateEvent) =>
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ extension AnalyticsRoomExtension on Room {
|
|||
return;
|
||||
}
|
||||
|
||||
for (final Room space in (await client.spacesImStudyingIn)) {
|
||||
for (final Room space in (await client.spaceImAStudentIn)) {
|
||||
if (space.spaceChildren.any((sc) => sc.roomId == id)) continue;
|
||||
await space.addAnalyticsRoomToSpace(this);
|
||||
}
|
||||
|
|
@ -175,7 +175,7 @@ extension AnalyticsRoomExtension on Room {
|
|||
return;
|
||||
}
|
||||
|
||||
for (final Room space in (await client.spacesImStudyingIn)) {
|
||||
for (final Room space in (await client.spaceImAStudentIn)) {
|
||||
await space.inviteSpaceTeachersToAnalyticsRoom(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -194,7 +194,7 @@ extension AnalyticsRoomExtension on Room {
|
|||
final List<Event> events = await getEventsBySender(
|
||||
type: type,
|
||||
sender: userId,
|
||||
count: 1,
|
||||
count: 10,
|
||||
);
|
||||
if (events.isEmpty) return null;
|
||||
final Event event = events.first;
|
||||
|
|
@ -249,4 +249,31 @@ extension AnalyticsRoomExtension on Room {
|
|||
return creationContent?.tryGet<String>(ModelKey.langCode) == langCode ||
|
||||
creationContent?.tryGet<String>(ModelKey.oldLangCode) == langCode;
|
||||
}
|
||||
|
||||
Future<String?> sendSummaryAnalyticsEvent(
|
||||
List<RecentMessageRecord> records,
|
||||
) async {
|
||||
final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel(
|
||||
messages: records,
|
||||
);
|
||||
final String? eventId = await sendEvent(
|
||||
analyticsModel.toJson(),
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
);
|
||||
return eventId;
|
||||
}
|
||||
|
||||
Future<String?> sendConstructsEvent(
|
||||
List<OneConstructUse> uses,
|
||||
) async {
|
||||
final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel(
|
||||
uses: uses,
|
||||
);
|
||||
|
||||
final String? eventId = await sendEvent(
|
||||
constructsModel.toJson(),
|
||||
type: PangeaEventTypes.construct,
|
||||
);
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,20 @@ import 'dart:convert';
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/lemma.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/models/speech_to_text_models.dart';
|
||||
|
|
@ -22,7 +28,7 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../widgets/matrix.dart';
|
||||
import '../constants/language_keys.dart';
|
||||
import '../constants/language_constants.dart';
|
||||
import '../constants/pangea_event_types.dart';
|
||||
import '../enum/use_type.dart';
|
||||
import '../utils/error_handler.dart';
|
||||
|
|
@ -31,7 +37,6 @@ class PangeaMessageEvent {
|
|||
late Event _event;
|
||||
final Timeline timeline;
|
||||
final bool ownMessage;
|
||||
bool _isValidPangeaMessageEvent = true;
|
||||
|
||||
PangeaMessageEvent({
|
||||
required Event event,
|
||||
|
|
@ -39,7 +44,7 @@ class PangeaMessageEvent {
|
|||
required this.ownMessage,
|
||||
}) {
|
||||
if (event.type != EventTypes.Message) {
|
||||
_isValidPangeaMessageEvent = false;
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "${event.type} should not be used to make a PangeaMessageEvent",
|
||||
);
|
||||
|
|
@ -542,7 +547,18 @@ class PangeaMessageEvent {
|
|||
originalWritten: false,
|
||||
);
|
||||
|
||||
UseType get useType => useTypeCalculator(originalSent?.choreo);
|
||||
UseType get msgUseType {
|
||||
final ChoreoRecord? choreoRecord = originalSent?.choreo;
|
||||
if (choreoRecord == null) {
|
||||
return UseType.un;
|
||||
} else if (choreoRecord.includedIT) {
|
||||
return UseType.ta;
|
||||
} else if (choreoRecord.hasAcceptedMatches) {
|
||||
return UseType.ga;
|
||||
} else {
|
||||
return UseType.wa;
|
||||
}
|
||||
}
|
||||
|
||||
bool get showUseType =>
|
||||
!ownMessage &&
|
||||
|
|
@ -651,21 +667,169 @@ class PangeaMessageEvent {
|
|||
}
|
||||
|
||||
/// Returns a list of [PracticeActivityEvent] for the user's active l2.
|
||||
List<PracticeActivityEvent> get practiceActivities {
|
||||
final String? l2code =
|
||||
MatrixState.pangeaController.languageController.activeL2Code();
|
||||
if (l2code == null) return [];
|
||||
return practiceActivitiesByLangCode(l2code);
|
||||
List<PracticeActivityEvent> get practiceActivities =>
|
||||
l2Code == null ? [] : practiceActivitiesByLangCode(l2Code!);
|
||||
|
||||
/// all construct uses for the message, including vocab and grammar
|
||||
List<OneConstructUse> get allConstructUses =>
|
||||
[..._grammarConstructUses, ..._vocabUses, ..._itStepsToConstructUses];
|
||||
|
||||
/// Returns a list of [OneConstructUse] from itSteps for which the continuance
|
||||
/// was selected or ignored. Correct selections are considered in the tokens
|
||||
/// flow. Once all continuances have lemmas, we can do both correct and incorrect
|
||||
/// in this flow. It actually doesn't do anything at all right now, because the
|
||||
/// choregrapher is not returning lemmas for continuances. This is a TODO.
|
||||
/// So currently only the lemmas can be gotten from the tokens for choices that
|
||||
/// are actually in the final message.
|
||||
List<OneConstructUse> get _itStepsToConstructUses {
|
||||
final List<OneConstructUse> uses = [];
|
||||
if (originalSent?.choreo == null) return uses;
|
||||
|
||||
for (final itStep in originalSent!.choreo!.itSteps) {
|
||||
for (final continuance in itStep.continuances) {
|
||||
// this seems to always be false for continuances right now
|
||||
|
||||
if (originalSent!.choreo!.finalMessage.contains(continuance.text)) {
|
||||
continue;
|
||||
}
|
||||
if (continuance.wasClicked) {
|
||||
//PTODO - account for end of flow score
|
||||
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
|
||||
uses.addAll(
|
||||
_lemmasToVocabUses(
|
||||
continuance.lemmas,
|
||||
ConstructUseTypeEnum.incIt,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
|
||||
uses.addAll(
|
||||
_lemmasToVocabUses(
|
||||
continuance.lemmas,
|
||||
ConstructUseTypeEnum.ignIt,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
// 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
|
||||
/// get construct uses of type vocab for the message
|
||||
List<OneConstructUse> get _vocabUses {
|
||||
final List<OneConstructUse> uses = [];
|
||||
|
||||
// replication of logic from message_content.dart
|
||||
// bool get isHtml =>
|
||||
// AppConfig.renderHtml && !_event.redacted && _event.isRichMessage;
|
||||
// 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 {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import 'package:matrix/src/utils/markdown.dart';
|
|||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../widgets/matrix.dart';
|
||||
import '../constants/language_keys.dart';
|
||||
import '../constants/language_constants.dart';
|
||||
import '../constants/pangea_event_types.dart';
|
||||
import '../models/choreo_record.dart';
|
||||
import '../models/representation_content_model.dart';
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../constants/pangea_event_types.dart';
|
||||
|
||||
class PracticeActivityRecordEvent {
|
||||
Event event;
|
||||
|
||||
PracticeActivityRecordModel? _content;
|
||||
|
||||
PracticeActivityRecordEvent({required this.event}) {
|
||||
if (event.type != PangeaEventTypes.activityRecord) {
|
||||
throw Exception(
|
||||
"${event.type} should not be used to make a PracticeActivityRecordEvent",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PracticeActivityRecordModel? get record {
|
||||
_content ??= event.getPangeaContent<PracticeActivityRecordModel>();
|
||||
return _content!;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -71,7 +71,9 @@ class PracticeActivityEvent {
|
|||
return records.firstOrNull;
|
||||
}
|
||||
|
||||
/// Checks if there is a user record for this activity,
|
||||
String get parentMessageId => event.relationshipEventId!;
|
||||
|
||||
/// Checks if there are any user records in the list for this activity,
|
||||
/// and, if so, then the activity is complete
|
||||
bool get isComplete => userRecord != null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../constants/pangea_event_types.dart';
|
||||
|
||||
class PracticeActivityRecordEvent {
|
||||
Event event;
|
||||
|
||||
PracticeActivityRecordModel? _content;
|
||||
|
||||
PracticeActivityRecordEvent({required this.event}) {
|
||||
if (event.type != PangeaEventTypes.activityRecord) {
|
||||
throw Exception(
|
||||
"${event.type} should not be used to make a PracticeActivityRecordEvent",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PracticeActivityRecordModel get record {
|
||||
_content ??= event.getPangeaContent<PracticeActivityRecordModel>();
|
||||
return _content!;
|
||||
}
|
||||
|
||||
Future<List<OneConstructUse>> uses(Timeline timeline) async {
|
||||
try {
|
||||
final String? parent = event.relationshipEventId;
|
||||
if (parent == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "PracticeActivityRecordEvent has null event.relationshipEventId",
|
||||
data: event.toJson(),
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
final Event? practiceEvent =
|
||||
await timeline.getEventById(event.relationshipEventId!);
|
||||
|
||||
if (practiceEvent == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "PracticeActivityRecordEvent has null practiceActivityEvent with id $parent",
|
||||
data: event.toJson(),
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
final PracticeActivityEvent practiceActivity = PracticeActivityEvent(
|
||||
event: practiceEvent,
|
||||
timeline: timeline,
|
||||
);
|
||||
|
||||
final List<OneConstructUse> uses = [];
|
||||
|
||||
final List<ConstructIdentifier> constructIds =
|
||||
practiceActivity.practiceActivity.tgtConstructs;
|
||||
|
||||
for (final construct in constructIds) {
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
lemma: construct.lemma,
|
||||
constructType: construct.type,
|
||||
useType: record.useType,
|
||||
//TODO - find form of construct within the message
|
||||
//this is related to the feature of highlighting the target construct in the message
|
||||
form: construct.lemma,
|
||||
chatId: event.roomId ?? practiceEvent.roomId ?? timeline.room.id,
|
||||
msgId: practiceActivity.parentMessageId,
|
||||
timeStamp: event.originServerTs,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return uses;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s, data: event.toJson());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
|
|
@ -28,32 +26,4 @@ abstract class AnalyticsEvent {
|
|||
}
|
||||
return contentCache!;
|
||||
}
|
||||
|
||||
static List<String> analyticsEventTypes = [
|
||||
PangeaEventTypes.summaryAnalytics,
|
||||
PangeaEventTypes.construct,
|
||||
];
|
||||
|
||||
static Future<String?> sendEvent(
|
||||
Room analyticsRoom,
|
||||
String type,
|
||||
List<dynamic> analyticsContent,
|
||||
) async {
|
||||
String? eventId;
|
||||
switch (type) {
|
||||
case PangeaEventTypes.summaryAnalytics:
|
||||
eventId = await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent(
|
||||
analyticsRoom,
|
||||
analyticsContent.cast<RecentMessageRecord>(),
|
||||
);
|
||||
break;
|
||||
case PangeaEventTypes.construct:
|
||||
eventId = await ConstructAnalyticsEvent.sendConstructsEvent(
|
||||
analyticsRoom,
|
||||
analyticsContent.cast<OneConstructUse>(),
|
||||
);
|
||||
break;
|
||||
}
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ abstract class AnalyticsModel {
|
|||
case PangeaEventTypes.summaryAnalytics:
|
||||
return SummaryAnalyticsModel.formatSummaryContent(recentMsgs);
|
||||
case PangeaEventTypes.construct:
|
||||
return ConstructAnalyticsModel.formatConstructsContent(recentMsgs);
|
||||
final List<OneConstructUse> uses = [];
|
||||
for (final msg in recentMsgs) {
|
||||
uses.addAll(msg.allConstructUses);
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,19 +18,4 @@ class ConstructAnalyticsEvent extends AnalyticsEvent {
|
|||
contentCache ??= ConstructAnalyticsModel.fromJson(event.content);
|
||||
return contentCache as ConstructAnalyticsModel;
|
||||
}
|
||||
|
||||
static Future<String?> sendConstructsEvent(
|
||||
Room analyticsRoom,
|
||||
List<OneConstructUse> uses,
|
||||
) async {
|
||||
final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel(
|
||||
uses: uses,
|
||||
);
|
||||
|
||||
final String? eventId = await analyticsRoom.sendEvent(
|
||||
constructsModel.toJson(),
|
||||
type: PangeaEventTypes.construct,
|
||||
);
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/analytics_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../enum/construct_type_enum.dart';
|
||||
|
|
@ -24,7 +22,7 @@ class ConstructAnalyticsModel extends AnalyticsModel {
|
|||
if (json[_usesKey] is List) {
|
||||
// This is the new format
|
||||
uses.addAll(
|
||||
json[_usesKey]
|
||||
(json[_usesKey] as List)
|
||||
.map((use) => OneConstructUse.fromJson(use))
|
||||
.cast<OneConstructUse>()
|
||||
.toList(),
|
||||
|
|
@ -39,13 +37,13 @@ class ConstructAnalyticsModel extends AnalyticsModel {
|
|||
final lemmaUses = useValue[_usesKey];
|
||||
for (final useData in lemmaUses) {
|
||||
final use = OneConstructUse(
|
||||
useType: ConstructUseType.ga,
|
||||
useType: ConstructUseTypeEnum.ga,
|
||||
chatId: useData["chatId"],
|
||||
timeStamp: DateTime.parse(useData["timeStamp"]),
|
||||
lemma: lemma,
|
||||
form: useData["form"],
|
||||
msgId: useData["msgId"],
|
||||
constructType: ConstructType.grammar,
|
||||
constructType: ConstructTypeEnum.grammar,
|
||||
);
|
||||
uses.add(use);
|
||||
}
|
||||
|
|
@ -70,122 +68,13 @@ class ConstructAnalyticsModel extends AnalyticsModel {
|
|||
_usesKey: uses.map((use) => use.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
static List<OneConstructUse> formatConstructsContent(
|
||||
List<PangeaMessageEvent> recentMsgs,
|
||||
) {
|
||||
final List<PangeaMessageEvent> filtered = List.from(recentMsgs);
|
||||
final List<OneConstructUse> uses = [];
|
||||
|
||||
for (final msg in filtered) {
|
||||
if (msg.originalSent?.choreo == null) continue;
|
||||
uses.addAll(
|
||||
msg.originalSent!.choreo!.toGrammarConstructUse(
|
||||
msg.eventId,
|
||||
msg.room.id,
|
||||
msg.originServerTs,
|
||||
),
|
||||
);
|
||||
|
||||
final List<PangeaToken>? tokens = msg.originalSent?.tokens;
|
||||
if (tokens == null) continue;
|
||||
uses.addAll(
|
||||
msg.originalSent!.choreo!.toVocabUse(
|
||||
tokens,
|
||||
msg.room.id,
|
||||
msg.eventId,
|
||||
msg.originServerTs,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return uses;
|
||||
}
|
||||
}
|
||||
|
||||
enum ConstructUseType {
|
||||
/// produced in chat by user, igc was run, and we've judged it to be a correct use
|
||||
wa,
|
||||
|
||||
/// produced in chat by user, igc was run, and we've judged it to be a incorrect use
|
||||
/// Note: if the IGC match is ignored, this is not counted as an incorrect use
|
||||
ga,
|
||||
|
||||
/// produced in chat by user and igc was not run
|
||||
unk,
|
||||
|
||||
/// selected correctly in IT flow
|
||||
corIt,
|
||||
|
||||
/// encountered as IT distractor and correctly ignored it
|
||||
ignIt,
|
||||
|
||||
/// encountered as it distractor and selected it
|
||||
incIt,
|
||||
|
||||
/// encountered in igc match and ignored match
|
||||
ignIGC,
|
||||
|
||||
/// selected correctly in IGC flow
|
||||
corIGC,
|
||||
|
||||
/// encountered as distractor in IGC flow and selected it
|
||||
incIGC,
|
||||
}
|
||||
|
||||
extension on ConstructUseType {
|
||||
String get string {
|
||||
switch (this) {
|
||||
case ConstructUseType.ga:
|
||||
return 'ga';
|
||||
case ConstructUseType.wa:
|
||||
return 'wa';
|
||||
case ConstructUseType.corIt:
|
||||
return 'corIt';
|
||||
case ConstructUseType.incIt:
|
||||
return 'incIt';
|
||||
case ConstructUseType.ignIt:
|
||||
return 'ignIt';
|
||||
case ConstructUseType.ignIGC:
|
||||
return 'ignIGC';
|
||||
case ConstructUseType.corIGC:
|
||||
return 'corIGC';
|
||||
case ConstructUseType.incIGC:
|
||||
return 'incIGC';
|
||||
case ConstructUseType.unk:
|
||||
return 'unk';
|
||||
}
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case ConstructUseType.ga:
|
||||
return Icons.check;
|
||||
case ConstructUseType.wa:
|
||||
return Icons.thumb_up_sharp;
|
||||
case ConstructUseType.corIt:
|
||||
return Icons.check;
|
||||
case ConstructUseType.incIt:
|
||||
return Icons.close;
|
||||
case ConstructUseType.ignIt:
|
||||
return Icons.close;
|
||||
case ConstructUseType.ignIGC:
|
||||
return Icons.close;
|
||||
case ConstructUseType.corIGC:
|
||||
return Icons.check;
|
||||
case ConstructUseType.incIGC:
|
||||
return Icons.close;
|
||||
case ConstructUseType.unk:
|
||||
return Icons.help;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OneConstructUse {
|
||||
String? lemma;
|
||||
ConstructType? constructType;
|
||||
ConstructTypeEnum? constructType;
|
||||
String? form;
|
||||
ConstructUseType useType;
|
||||
ConstructUseTypeEnum useType;
|
||||
String chatId;
|
||||
String? msgId;
|
||||
DateTime timeStamp;
|
||||
|
|
@ -204,7 +93,7 @@ class OneConstructUse {
|
|||
|
||||
factory OneConstructUse.fromJson(Map<String, dynamic> json) {
|
||||
return OneConstructUse(
|
||||
useType: ConstructUseType.values
|
||||
useType: ConstructUseTypeEnum.values
|
||||
.firstWhere((e) => e.string == json['useType']),
|
||||
chatId: json['chatId'],
|
||||
timeStamp: DateTime.parse(json['timeStamp']),
|
||||
|
|
@ -248,7 +137,7 @@ class OneConstructUse {
|
|||
|
||||
class ConstructUses {
|
||||
final List<OneConstructUse> uses;
|
||||
final ConstructType constructType;
|
||||
final ConstructTypeEnum constructType;
|
||||
final String lemma;
|
||||
|
||||
ConstructUses({
|
||||
|
|
|
|||
|
|
@ -18,18 +18,4 @@ class SummaryAnalyticsEvent extends AnalyticsEvent {
|
|||
contentCache ??= SummaryAnalyticsModel.fromJson(event.content);
|
||||
return contentCache as SummaryAnalyticsModel;
|
||||
}
|
||||
|
||||
static Future<String?> sendSummaryAnalyticsEvent(
|
||||
Room analyticsRoom,
|
||||
List<RecentMessageRecord> records,
|
||||
) async {
|
||||
final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel(
|
||||
messages: records,
|
||||
);
|
||||
final String? eventId = await analyticsRoom.sendEvent(
|
||||
analyticsModel.toJson(),
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
);
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class SummaryAnalyticsModel extends AnalyticsModel {
|
|||
(msg) => RecentMessageRecord(
|
||||
eventId: msg.eventId,
|
||||
chatId: msg.room.id,
|
||||
useType: msg.useType,
|
||||
useType: msg.msgUseType,
|
||||
time: msg.originServerTs,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
|
||||
import '../constants/choreo_constants.dart';
|
||||
import '../enum/construct_type_enum.dart';
|
||||
import 'it_step.dart';
|
||||
import 'lemma.dart';
|
||||
|
||||
/// this class lives within a [PangeaIGCEvent]
|
||||
/// it always has a [RepresentationEvent] parent
|
||||
|
|
@ -111,135 +106,6 @@ class ChoreoRecord {
|
|||
openMatches: [],
|
||||
);
|
||||
|
||||
/// [tokens] is the final list of tokens that were sent
|
||||
/// if no ga or ta,
|
||||
/// make wa use for each and return
|
||||
/// else
|
||||
/// for each saveable vocab in the final message
|
||||
/// if vocab is contained in an accepted replacement, make ga use
|
||||
/// if vocab is contained in ta choice,
|
||||
/// if selected as choice, corIt
|
||||
/// if written as customInput, corIt? (account for score in this)
|
||||
/// for each it step
|
||||
/// for each continuance
|
||||
/// if not within the final message, save ignIT/incIT
|
||||
List<OneConstructUse> toVocabUse(
|
||||
List<PangeaToken> tokens,
|
||||
String chatId,
|
||||
String msgId,
|
||||
DateTime timestamp,
|
||||
) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
final DateTime now = DateTime.now();
|
||||
List<OneConstructUse> lemmasToVocabUses(
|
||||
List<Lemma> lemmas,
|
||||
ConstructUseType type,
|
||||
) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
for (final lemma in lemmas) {
|
||||
if (lemma.saveVocab) {
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
useType: type,
|
||||
chatId: chatId,
|
||||
timeStamp: timestamp,
|
||||
lemma: lemma.text,
|
||||
form: lemma.form,
|
||||
msgId: msgId,
|
||||
constructType: ConstructType.vocab,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
List<OneConstructUse> getVocabUseForToken(PangeaToken token) {
|
||||
for (final step in choreoSteps) {
|
||||
/// if 1) accepted match 2) token is in the replacement and 3) replacement
|
||||
/// is in the overall step text, then token was a ga
|
||||
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted &&
|
||||
(step.acceptedOrIgnoredMatch!.match.choices?.any(
|
||||
(r) =>
|
||||
r.value.contains(token.text.content) &&
|
||||
step.text.contains(r.value),
|
||||
) ??
|
||||
false)) {
|
||||
return lemmasToVocabUses(token.lemmas, ConstructUseType.ga);
|
||||
}
|
||||
if (step.itStep != null) {
|
||||
final bool pickedThroughIT = step.itStep!.chosenContinuance?.text
|
||||
.contains(token.text.content) ??
|
||||
false;
|
||||
if (pickedThroughIT) {
|
||||
return lemmasToVocabUses(token.lemmas, ConstructUseType.corIt);
|
||||
//PTODO - check if added via custom input in IT flow
|
||||
}
|
||||
}
|
||||
}
|
||||
return lemmasToVocabUses(token.lemmas, ConstructUseType.wa);
|
||||
}
|
||||
|
||||
/// for each token, record whether selected in ga, ta, or wa
|
||||
for (final token in tokens) {
|
||||
uses.addAll(getVocabUseForToken(token));
|
||||
}
|
||||
|
||||
for (final itStep in itSteps) {
|
||||
for (final continuance in itStep.continuances) {
|
||||
// this seems to always be false for continuances right now
|
||||
|
||||
if (finalMessage.contains(continuance.text)) {
|
||||
continue;
|
||||
}
|
||||
if (continuance.wasClicked) {
|
||||
//PTODO - account for end of flow score
|
||||
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
|
||||
uses.addAll(
|
||||
lemmasToVocabUses(continuance.lemmas, ConstructUseType.incIt),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
|
||||
uses.addAll(
|
||||
lemmasToVocabUses(continuance.lemmas, ConstructUseType.ignIt),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uses;
|
||||
}
|
||||
|
||||
List<OneConstructUse> toGrammarConstructUse(
|
||||
String msgId,
|
||||
String chatId,
|
||||
DateTime timestamp,
|
||||
) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
for (final step in choreoSteps) {
|
||||
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) {
|
||||
final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ??
|
||||
step.acceptedOrIgnoredMatch!.match.shortMessage ??
|
||||
step.acceptedOrIgnoredMatch!.match.type.typeName.name;
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
useType: ConstructUseType.ga,
|
||||
chatId: chatId,
|
||||
timeStamp: timestamp,
|
||||
lemma: name,
|
||||
form: name,
|
||||
msgId: msgId,
|
||||
constructType: ConstructType.grammar,
|
||||
id: "${msgId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
List<ITStep> get itSteps =>
|
||||
choreoSteps.where((e) => e.itStep != null).map((e) => e.itStep!).toList();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -154,32 +155,37 @@ class VocabTotals {
|
|||
void addVocabUseBasedOnUseType(List<OneConstructUse> uses) {
|
||||
for (final use in uses) {
|
||||
switch (use.useType) {
|
||||
case ConstructUseType.ga:
|
||||
case ConstructUseTypeEnum.ga:
|
||||
ga++;
|
||||
break;
|
||||
case ConstructUseType.wa:
|
||||
case ConstructUseTypeEnum.wa:
|
||||
wa++;
|
||||
break;
|
||||
case ConstructUseType.corIt:
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
corIt++;
|
||||
break;
|
||||
case ConstructUseType.incIt:
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
incIt++;
|
||||
break;
|
||||
case ConstructUseType.ignIt:
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
ignIt++;
|
||||
break;
|
||||
//TODO - these shouldn't be counted as such
|
||||
case ConstructUseType.ignIGC:
|
||||
case ConstructUseTypeEnum.ignIGC:
|
||||
ignIt++;
|
||||
break;
|
||||
case ConstructUseType.corIGC:
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
corIt++;
|
||||
break;
|
||||
case ConstructUseType.incIGC:
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
incIt++;
|
||||
break;
|
||||
case ConstructUseType.unk:
|
||||
//TODO if we bring back Headwords then we need to add these
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
break;
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
break;
|
||||
case ConstructUseTypeEnum.unk:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/language_detection_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/span_card_model.dart';
|
||||
|
|
@ -13,12 +14,11 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../constants/model_keys.dart';
|
||||
import 'language_detection_model.dart';
|
||||
|
||||
// import 'package:language_tool/language_tool.dart';
|
||||
|
||||
class IGCTextData {
|
||||
List<LanguageDetection> detections;
|
||||
LanguageDetectionResponse detections;
|
||||
String originalInput;
|
||||
String? fullTextCorrection;
|
||||
List<PangeaToken> tokens;
|
||||
|
|
@ -42,6 +42,18 @@ class IGCTextData {
|
|||
});
|
||||
|
||||
factory IGCTextData.fromJson(Map<String, dynamic> json) {
|
||||
// changing this to allow for use of the LanguageDetectionResponse methods
|
||||
// TODO - change API after we're sure all clients are updated. not urgent.
|
||||
final LanguageDetectionResponse detections =
|
||||
json[_detectionsKey] is Iterable
|
||||
? LanguageDetectionResponse.fromJson({
|
||||
"detections": json[_detectionsKey],
|
||||
"full_text": json["original_input"],
|
||||
})
|
||||
: LanguageDetectionResponse.fromJson(
|
||||
json[_detectionsKey] as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
return IGCTextData(
|
||||
tokens: (json[_tokensKey] as Iterable)
|
||||
.map<PangeaToken>(
|
||||
|
|
@ -59,12 +71,7 @@ class IGCTextData {
|
|||
.toList()
|
||||
.cast<PangeaMatch>()
|
||||
: [],
|
||||
detections: (json[_detectionsKey] as Iterable)
|
||||
.map<LanguageDetection>(
|
||||
(e) => LanguageDetection.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
.cast<LanguageDetection>(),
|
||||
detections: detections,
|
||||
originalInput: json["original_input"],
|
||||
fullTextCorrection: json["full_text_correction"],
|
||||
userL1: json[ModelKey.userL1],
|
||||
|
|
@ -79,7 +86,7 @@ class IGCTextData {
|
|||
static const String _detectionsKey = "detections";
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
_detectionsKey: detections.map((e) => e.toJson()).toList(),
|
||||
_detectionsKey: detections.toJson(),
|
||||
"original_input": originalInput,
|
||||
"full_text_correction": fullTextCorrection,
|
||||
_tokensKey: tokens.map((e) => e.toJson()).toList(),
|
||||
|
|
@ -90,6 +97,18 @@ class IGCTextData {
|
|||
"enable_igc": enableIGC,
|
||||
};
|
||||
|
||||
/// if we haven't run IGC or IT or there are no matches, we use the highest validated detection
|
||||
/// from [LanguageDetectionResponse.highestValidatedDetection]
|
||||
/// if we have run igc/it and there are no matches, we can relax the threshold
|
||||
/// and use the highest confidence detection
|
||||
String get detectedLanguage {
|
||||
if (!(enableIGC && enableIT) || matches.isNotEmpty) {
|
||||
return detections.highestValidatedDetection().langCode;
|
||||
} else {
|
||||
return detections.highestConfidenceDetection.langCode;
|
||||
}
|
||||
}
|
||||
|
||||
// reconstruct fullText based on accepted match
|
||||
//update offsets in existing matches to reflect the change
|
||||
//if existing matches overlap with the accepted one, remove them??
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
|
|
|||
|
|
@ -1,24 +1,51 @@
|
|||
/// 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});
|
||||
/// [pos] ex "v" - part of speech of the lemma
|
||||
/// https://universaldependencies.org/u/pos/
|
||||
final String pos;
|
||||
|
||||
/// [morph] ex {} - morphological features of the lemma
|
||||
/// https://universaldependencies.org/u/feat/
|
||||
final Map<String, dynamic> morph;
|
||||
|
||||
Lemma({
|
||||
required this.text,
|
||||
required this.saveVocab,
|
||||
required this.form,
|
||||
this.pos = '',
|
||||
this.morph = const {},
|
||||
});
|
||||
|
||||
factory Lemma.fromJson(Map<String, dynamic> json) {
|
||||
return Lemma(
|
||||
text: json['text'],
|
||||
saveVocab: json['save_vocab'] ?? json['saveVocab'] ?? false,
|
||||
form: json["form"] ?? json['text'],
|
||||
pos: json['pos'] ?? '',
|
||||
morph: json['morph'] ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {'text': text, 'save_vocab': saveVocab, 'form': form};
|
||||
return {
|
||||
'text': text,
|
||||
'save_vocab': saveVocab,
|
||||
'form': form,
|
||||
'pos': pos,
|
||||
'morph': morph,
|
||||
};
|
||||
}
|
||||
|
||||
static Lemma get empty => Lemma(text: '', saveVocab: true, form: '');
|
||||
|
||||
static Lemma create(String form) =>
|
||||
Lemma(text: '', saveVocab: true, form: form);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,10 @@ import 'lemma.dart';
|
|||
|
||||
class PangeaToken {
|
||||
PangeaTokenText text;
|
||||
bool hasInfo;
|
||||
List<Lemma> lemmas;
|
||||
|
||||
PangeaToken({
|
||||
required this.text,
|
||||
required this.hasInfo,
|
||||
required this.lemmas,
|
||||
});
|
||||
|
||||
|
|
@ -37,7 +35,6 @@ class PangeaToken {
|
|||
PangeaTokenText.fromJson(json[_textKey] as Map<String, dynamic>);
|
||||
return PangeaToken(
|
||||
text: text,
|
||||
hasInfo: json[_hasInfoKey] ?? text.length > 2,
|
||||
lemmas: getLemmas(text.content, json[_lemmaKey]),
|
||||
);
|
||||
} catch (err, s) {
|
||||
|
|
@ -56,12 +53,10 @@ class PangeaToken {
|
|||
}
|
||||
|
||||
static const String _textKey = "text";
|
||||
static const String _hasInfoKey = "has_info";
|
||||
static const String _lemmaKey = ModelKey.lemma;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
_textKey: text.toJson(),
|
||||
_hasInfoKey: hasInfo,
|
||||
_lemmaKey: lemmas.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart';
|
|||
|
||||
class ConstructIdentifier {
|
||||
final String lemma;
|
||||
final ConstructType type;
|
||||
final ConstructTypeEnum type;
|
||||
|
||||
ConstructIdentifier({required this.lemma, required this.type});
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ class ConstructIdentifier {
|
|||
try {
|
||||
return ConstructIdentifier(
|
||||
lemma: json['lemma'] as String,
|
||||
type: ConstructType.values.firstWhere(
|
||||
type: ConstructTypeEnum.values.firstWhere(
|
||||
(e) => e.string == json['type'],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,16 +5,18 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
|
||||
class PracticeActivityRecordModel {
|
||||
final String? question;
|
||||
late List<ActivityResponse> responses;
|
||||
late List<ActivityRecordResponse> responses;
|
||||
|
||||
PracticeActivityRecordModel({
|
||||
required this.question,
|
||||
List<ActivityResponse>? responses,
|
||||
List<ActivityRecordResponse>? responses,
|
||||
}) {
|
||||
if (responses == null) {
|
||||
this.responses = List<ActivityResponse>.empty(growable: true);
|
||||
this.responses = List<ActivityRecordResponse>.empty(growable: true);
|
||||
} else {
|
||||
this.responses = responses;
|
||||
}
|
||||
|
|
@ -26,7 +28,9 @@ class PracticeActivityRecordModel {
|
|||
return PracticeActivityRecordModel(
|
||||
question: json['question'] as String,
|
||||
responses: (json['responses'] as List)
|
||||
.map((e) => ActivityResponse.fromJson(e as Map<String, dynamic>))
|
||||
.map(
|
||||
(e) => ActivityRecordResponse.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
|
@ -40,26 +44,34 @@ class PracticeActivityRecordModel {
|
|||
|
||||
/// get the latest response index according to the response timeStamp
|
||||
/// sort the responses by timestamp and get the index of the last response
|
||||
String? get latestResponse {
|
||||
ActivityRecordResponse? get latestResponse {
|
||||
if (responses.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
responses.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
return responses[responses.length - 1].text;
|
||||
return responses[responses.length - 1];
|
||||
}
|
||||
|
||||
ConstructUseTypeEnum get useType => latestResponse?.score != null
|
||||
? (latestResponse!.score > 0
|
||||
? ConstructUseTypeEnum.corPA
|
||||
: ConstructUseTypeEnum.incPA)
|
||||
: ConstructUseTypeEnum.unk;
|
||||
|
||||
void addResponse({
|
||||
String? text,
|
||||
Uint8List? audioBytes,
|
||||
Uint8List? imageBytes,
|
||||
required double score,
|
||||
}) {
|
||||
try {
|
||||
responses.add(
|
||||
ActivityResponse(
|
||||
ActivityRecordResponse(
|
||||
text: text,
|
||||
audioBytes: audioBytes,
|
||||
imageBytes: imageBytes,
|
||||
timestamp: DateTime.now(),
|
||||
score: score,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
|
|
@ -84,27 +96,33 @@ class PracticeActivityRecordModel {
|
|||
int get hashCode => question.hashCode ^ responses.hashCode;
|
||||
}
|
||||
|
||||
class ActivityResponse {
|
||||
class ActivityRecordResponse {
|
||||
// the user's response
|
||||
// has nullable string, nullable audio bytes, nullable image bytes, and timestamp
|
||||
final String? text;
|
||||
final Uint8List? audioBytes;
|
||||
final Uint8List? imageBytes;
|
||||
final DateTime timestamp;
|
||||
final double score;
|
||||
|
||||
ActivityResponse({
|
||||
ActivityRecordResponse({
|
||||
this.text,
|
||||
this.audioBytes,
|
||||
this.imageBytes,
|
||||
required this.score,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
factory ActivityResponse.fromJson(Map<String, dynamic> json) {
|
||||
return ActivityResponse(
|
||||
factory ActivityRecordResponse.fromJson(Map<String, dynamic> json) {
|
||||
return ActivityRecordResponse(
|
||||
text: json['text'] as String?,
|
||||
audioBytes: json['audio'] as Uint8List?,
|
||||
imageBytes: json['image'] as Uint8List?,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
// this has a default of 1 to make this backwards compatible
|
||||
// score was added later and is not present in all records
|
||||
// currently saved to Matrix
|
||||
score: json['score'] ?? 1.0,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +132,7 @@ class ActivityResponse {
|
|||
'audio': audioBytes,
|
||||
'image': imageBytes,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'score': score,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +140,7 @@ class ActivityResponse {
|
|||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is ActivityResponse &&
|
||||
return other is ActivityRecordResponse &&
|
||||
other.text == text &&
|
||||
other.audioBytes == audioBytes &&
|
||||
other.imageBytes == imageBytes &&
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../constants/class_default_values.dart';
|
||||
import '../constants/language_keys.dart';
|
||||
import '../constants/language_constants.dart';
|
||||
import '../constants/pangea_event_types.dart';
|
||||
import 'language_model.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import 'package:country_picker/country_picker.dart';
|
|||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/instructions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../constants/language_keys.dart';
|
||||
import '../constants/language_constants.dart';
|
||||
import 'language_model.dart';
|
||||
|
||||
PUserModel pUserModelFromJson(String str) =>
|
||||
|
|
|
|||
|
|
@ -34,9 +34,7 @@ class AnalyticsLanguageButton extends StatelessWidget {
|
|||
}).toList(),
|
||||
child: TextButton.icon(
|
||||
label: Text(
|
||||
L10n.of(context)!.languageButtonLabel(
|
||||
value.getDisplayName(context) ?? value.langCode,
|
||||
),
|
||||
value.getDisplayName(context) ?? value.langCode,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
|
@ -141,9 +140,7 @@ class AnalyticsListTileState extends State<AnalyticsListTile> {
|
|||
return;
|
||||
}
|
||||
if ((room?.isSpace ?? false) && widget.allowNavigateOnSelect) {
|
||||
final String selectedView =
|
||||
widget.controller!.widget.selectedView!.route;
|
||||
context.go('/rooms/analytics/${room!.id}/$selectedView');
|
||||
context.go('/rooms/analytics/${room!.id}');
|
||||
return;
|
||||
}
|
||||
widget.onTap(widget.selected);
|
||||
|
|
|
|||
49
lib/pangea/pages/analytics/analytics_view_button.dart
Normal file
49
lib/pangea/pages/analytics/analytics_view_button.dart
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class AnalyticsViewButton extends StatelessWidget {
|
||||
final BarChartViewSelection value;
|
||||
final void Function(BarChartViewSelection) onChange;
|
||||
const AnalyticsViewButton({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChange,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<BarChartViewSelection>(
|
||||
tooltip: L10n.of(context)!.changeAnalyticsView,
|
||||
initialValue: value,
|
||||
onSelected: (BarChartViewSelection? view) {
|
||||
if (view == null) {
|
||||
debugPrint("when is view null?");
|
||||
return;
|
||||
}
|
||||
onChange(view);
|
||||
},
|
||||
itemBuilder: (BuildContext context) => BarChartViewSelection.values
|
||||
.map<PopupMenuEntry<BarChartViewSelection>>(
|
||||
(BarChartViewSelection view) {
|
||||
return PopupMenuItem<BarChartViewSelection>(
|
||||
value: view,
|
||||
child: Text(view.string(context)),
|
||||
);
|
||||
}).toList(),
|
||||
child: TextButton.icon(
|
||||
label: Text(
|
||||
value.string(context),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
value.icon,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
onPressed: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ import '../../models/analytics/chart_analytics_model.dart';
|
|||
class BaseAnalyticsPage extends StatefulWidget {
|
||||
final String pageTitle;
|
||||
final List<TabData> tabs;
|
||||
final BarChartViewSelection? selectedView;
|
||||
final BarChartViewSelection selectedView;
|
||||
|
||||
final AnalyticsSelected defaultSelected;
|
||||
final AnalyticsSelected? alwaysSelected;
|
||||
|
|
@ -33,7 +33,7 @@ class BaseAnalyticsPage extends StatefulWidget {
|
|||
required this.tabs,
|
||||
required this.alwaysSelected,
|
||||
required this.defaultSelected,
|
||||
this.selectedView,
|
||||
required this.selectedView,
|
||||
this.myAnalyticsController,
|
||||
targetLanguages,
|
||||
}) : targetLanguages = (targetLanguages?.isNotEmpty ?? false)
|
||||
|
|
@ -50,6 +50,7 @@ class BaseAnalyticsController extends State<BaseAnalyticsPage> {
|
|||
String? currentLemma;
|
||||
ChartAnalyticsModel? chartData;
|
||||
StreamController refreshStream = StreamController.broadcast();
|
||||
BarChartViewSelection currentView = BarChartViewSelection.messages;
|
||||
|
||||
bool isSelected(String chatOrStudentId) => chatOrStudentId == selected?.id;
|
||||
|
||||
|
|
@ -63,6 +64,7 @@ class BaseAnalyticsController extends State<BaseAnalyticsPage> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
currentView = widget.selectedView;
|
||||
if (widget.defaultSelected.type == AnalyticsEntryType.student) {
|
||||
runFirstRefresh();
|
||||
}
|
||||
|
|
@ -168,6 +170,12 @@ class BaseAnalyticsController extends State<BaseAnalyticsPage> {
|
|||
refreshStream.add(false);
|
||||
}
|
||||
|
||||
Future<void> toggleView(BarChartViewSelection view) async {
|
||||
currentView = view;
|
||||
await setChartData();
|
||||
refreshStream.add(false);
|
||||
}
|
||||
|
||||
void setCurrentLemma(String? lemma) {
|
||||
currentLemma = lemma;
|
||||
setState(() {});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
|||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/analytics_language_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/analytics_view_button.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/construct_list.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/messages_bar_chart.dart';
|
||||
|
|
@ -24,18 +25,14 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
final BaseAnalyticsController controller;
|
||||
|
||||
Widget chartView(BuildContext context) {
|
||||
if (controller.widget.selectedView == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
switch (controller.widget.selectedView!) {
|
||||
switch (controller.currentView) {
|
||||
case BarChartViewSelection.messages:
|
||||
return MessagesBarChart(
|
||||
chartAnalytics: controller.chartData,
|
||||
);
|
||||
case BarChartViewSelection.grammar:
|
||||
return ConstructList(
|
||||
constructType: ConstructType.grammar,
|
||||
constructType: ConstructTypeEnum.grammar,
|
||||
defaultSelected: controller.widget.defaultSelected,
|
||||
selected: controller.selected,
|
||||
controller: controller,
|
||||
|
|
@ -75,27 +72,13 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
if (controller.activeSpace != null)
|
||||
TextSpan(
|
||||
text: controller.activeSpace!.getLocalizedDisplayname(),
|
||||
style: const TextStyle(decoration: TextDecoration.underline),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
if (controller.widget.selectedView == null) return;
|
||||
String route =
|
||||
"/rooms/${controller.widget.defaultSelected.type.route}";
|
||||
if (controller.widget.defaultSelected.type ==
|
||||
AnalyticsEntryType.space) {
|
||||
route += "/${controller.widget.defaultSelected.id}";
|
||||
}
|
||||
context.go(route);
|
||||
},
|
||||
),
|
||||
if (controller.widget.selectedView != null)
|
||||
const TextSpan(
|
||||
text: " > ",
|
||||
),
|
||||
if (controller.widget.selectedView != null)
|
||||
TextSpan(
|
||||
text: controller.widget.selectedView!.string(context),
|
||||
),
|
||||
const TextSpan(
|
||||
text: " > ",
|
||||
),
|
||||
TextSpan(
|
||||
text: controller.currentView.string(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
|
@ -104,220 +87,156 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: false,
|
||||
child: controller.widget.selectedView != null
|
||||
? Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// if (controller.widget.defaultSelected.type ==
|
||||
// AnalyticsEntryType.student)
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.refresh),
|
||||
// onPressed: controller.onRefresh,
|
||||
// tooltip: L10n.of(context)!.refresh,
|
||||
// ),
|
||||
TimeSpanMenuButton(
|
||||
value: controller.currentTimeSpan,
|
||||
onChange: (TimeSpan value) =>
|
||||
controller.toggleTimeSpan(context, value),
|
||||
),
|
||||
AnalyticsLanguageButton(
|
||||
value: controller
|
||||
.pangeaController.analytics.currentAnalyticsLang,
|
||||
onChange: (lang) => controller.toggleSpaceLang(lang),
|
||||
languages: controller.widget.targetLanguages,
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: chartView(context),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: DefaultTabController(
|
||||
length: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
tabs: [
|
||||
...controller.widget.tabs.map(
|
||||
(tab) => Tab(
|
||||
icon: Icon(
|
||||
tab.icon,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TimeSpanMenuButton(
|
||||
value: controller.currentTimeSpan,
|
||||
onChange: (TimeSpan value) =>
|
||||
controller.toggleTimeSpan(context, value),
|
||||
),
|
||||
AnalyticsViewButton(
|
||||
value: controller.currentView,
|
||||
onChange: controller.toggleView,
|
||||
),
|
||||
AnalyticsLanguageButton(
|
||||
value: controller
|
||||
.pangeaController.analytics.currentAnalyticsLang,
|
||||
onChange: (lang) => controller.toggleSpaceLang(lang),
|
||||
languages: controller.widget.targetLanguages,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: chartView(context),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: DefaultTabController(
|
||||
length: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
tabs: [
|
||||
...controller.widget.tabs.map(
|
||||
(tab) => Tab(
|
||||
icon: Icon(
|
||||
tab.icon,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
height: max(
|
||||
controller.widget.tabs[0].items.length + 1,
|
||||
controller.widget.tabs[1].items.length,
|
||||
) *
|
||||
72,
|
||||
child: TabBarView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
...controller.widget.tabs[0].items.map(
|
||||
(item) => AnalyticsListTile(
|
||||
refreshStream: controller.refreshStream,
|
||||
avatar: item.avatar,
|
||||
defaultSelected:
|
||||
controller.widget.defaultSelected,
|
||||
selected: AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[0].type,
|
||||
item.displayName,
|
||||
),
|
||||
isSelected:
|
||||
controller.isSelected(item.id),
|
||||
onTap: (_) => controller.toggleSelection(
|
||||
AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[0].type,
|
||||
item.displayName,
|
||||
),
|
||||
),
|
||||
allowNavigateOnSelect: controller
|
||||
.widget.tabs[0].allowNavigateOnSelect,
|
||||
pangeaController:
|
||||
controller.pangeaController,
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.widget.defaultSelected.type ==
|
||||
AnalyticsEntryType.space)
|
||||
AnalyticsListTile(
|
||||
refreshStream: controller.refreshStream,
|
||||
defaultSelected:
|
||||
controller.widget.defaultSelected,
|
||||
avatar: null,
|
||||
selected: AnalyticsSelected(
|
||||
controller.widget.defaultSelected.id,
|
||||
AnalyticsEntryType.privateChats,
|
||||
L10n.of(context)!.allPrivateChats,
|
||||
),
|
||||
allowNavigateOnSelect: false,
|
||||
isSelected: controller.isSelected(
|
||||
controller.widget.defaultSelected.id,
|
||||
),
|
||||
onTap: controller.toggleSelection,
|
||||
pangeaController:
|
||||
controller.pangeaController,
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: controller.widget.tabs[1].items
|
||||
.map(
|
||||
(item) => AnalyticsListTile(
|
||||
refreshStream: controller.refreshStream,
|
||||
avatar: item.avatar,
|
||||
defaultSelected:
|
||||
controller.widget.defaultSelected,
|
||||
selected: AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[1].type,
|
||||
item.displayName,
|
||||
),
|
||||
isSelected:
|
||||
controller.isSelected(item.id),
|
||||
onTap: controller.toggleSelection,
|
||||
allowNavigateOnSelect: controller.widget
|
||||
.tabs[1].allowNavigateOnSelect,
|
||||
pangeaController:
|
||||
controller.pangeaController,
|
||||
controller: controller,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
height: max(
|
||||
controller.widget.tabs[0].items.length +
|
||||
1,
|
||||
controller.widget.tabs[1].items.length,
|
||||
) *
|
||||
72,
|
||||
child: TabBarView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
...controller.widget.tabs[0].items.map(
|
||||
(item) => AnalyticsListTile(
|
||||
refreshStream:
|
||||
controller.refreshStream,
|
||||
avatar: item.avatar,
|
||||
defaultSelected: controller
|
||||
.widget.defaultSelected,
|
||||
selected: AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[0].type,
|
||||
item.displayName,
|
||||
),
|
||||
isSelected:
|
||||
controller.isSelected(item.id),
|
||||
onTap: (_) =>
|
||||
controller.toggleSelection(
|
||||
AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[0].type,
|
||||
item.displayName,
|
||||
),
|
||||
),
|
||||
allowNavigateOnSelect: controller
|
||||
.widget
|
||||
.tabs[0]
|
||||
.allowNavigateOnSelect,
|
||||
pangeaController:
|
||||
controller.pangeaController,
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
if (controller
|
||||
.widget.defaultSelected.type ==
|
||||
AnalyticsEntryType.space)
|
||||
AnalyticsListTile(
|
||||
refreshStream:
|
||||
controller.refreshStream,
|
||||
defaultSelected: controller
|
||||
.widget.defaultSelected,
|
||||
avatar: null,
|
||||
selected: AnalyticsSelected(
|
||||
controller
|
||||
.widget.defaultSelected.id,
|
||||
AnalyticsEntryType.privateChats,
|
||||
L10n.of(context)!.allPrivateChats,
|
||||
),
|
||||
allowNavigateOnSelect: false,
|
||||
isSelected: controller.isSelected(
|
||||
controller
|
||||
.widget.defaultSelected.id,
|
||||
),
|
||||
onTap: controller.toggleSelection,
|
||||
pangeaController:
|
||||
controller.pangeaController,
|
||||
controller: controller,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
children: controller.widget.tabs[1].items
|
||||
.map(
|
||||
(item) => AnalyticsListTile(
|
||||
refreshStream:
|
||||
controller.refreshStream,
|
||||
avatar: item.avatar,
|
||||
defaultSelected: controller
|
||||
.widget.defaultSelected,
|
||||
selected: AnalyticsSelected(
|
||||
item.id,
|
||||
controller.widget.tabs[1].type,
|
||||
item.displayName,
|
||||
),
|
||||
isSelected: controller
|
||||
.isSelected(item.id),
|
||||
onTap: controller.toggleSelection,
|
||||
allowNavigateOnSelect: controller
|
||||
.widget
|
||||
.tabs[1]
|
||||
.allowNavigateOnSelect,
|
||||
pangeaController:
|
||||
controller.pangeaController,
|
||||
controller: controller,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.grammarAnalytics),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor:
|
||||
Theme.of(context).textTheme.bodyLarge!.color,
|
||||
child: Icon(BarChartViewSelection.grammar.icon),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
String route =
|
||||
"/rooms/${controller.widget.defaultSelected.type.route}";
|
||||
if (controller.widget.defaultSelected.type ==
|
||||
AnalyticsEntryType.space) {
|
||||
route += "/${controller.widget.defaultSelected.id}";
|
||||
}
|
||||
route += "/${BarChartViewSelection.grammar.route}";
|
||||
context.go(route);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.messageAnalytics),
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
foregroundColor:
|
||||
Theme.of(context).textTheme.bodyLarge!.color,
|
||||
child: Icon(BarChartViewSelection.messages.icon),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
String route =
|
||||
"/rooms/${controller.widget.defaultSelected.type.route}";
|
||||
if (controller.widget.defaultSelected.type ==
|
||||
AnalyticsEntryType.space) {
|
||||
route += "/${controller.widget.defaultSelected.id}";
|
||||
}
|
||||
route += "/${BarChartViewSelection.messages.route}";
|
||||
context.go(route);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import '../../../utils/sync_status_util_v2.dart';
|
|||
import 'space_analytics_view.dart';
|
||||
|
||||
class SpaceAnalyticsPage extends StatefulWidget {
|
||||
final BarChartViewSelection? selectedView;
|
||||
const SpaceAnalyticsPage({super.key, this.selectedView});
|
||||
final BarChartViewSelection selectedView;
|
||||
const SpaceAnalyticsPage({super.key, required this.selectedView});
|
||||
|
||||
@override
|
||||
State<SpaceAnalyticsPage> createState() => SpaceAnalyticsV2Controller();
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -19,8 +18,8 @@ import '../base_analytics.dart';
|
|||
import 'student_analytics_view.dart';
|
||||
|
||||
class StudentAnalyticsPage extends StatefulWidget {
|
||||
final BarChartViewSelection? selectedView;
|
||||
const StudentAnalyticsPage({super.key, this.selectedView});
|
||||
final BarChartViewSelection selectedView;
|
||||
const StudentAnalyticsPage({super.key, required this.selectedView});
|
||||
|
||||
@override
|
||||
State<StudentAnalyticsPage> createState() => StudentAnalyticsController();
|
||||
|
|
@ -29,49 +28,35 @@ class StudentAnalyticsPage extends StatefulWidget {
|
|||
class StudentAnalyticsController extends State<StudentAnalyticsPage> {
|
||||
final PangeaController _pangeaController = MatrixState.pangeaController;
|
||||
AnalyticsSelected? selected;
|
||||
StreamSubscription? stateSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final listFutures = [
|
||||
_pangeaController.myAnalytics.setStudentChats(),
|
||||
_pangeaController.myAnalytics.setStudentSpaces(),
|
||||
];
|
||||
Future.wait(listFutures).then((_) => setState(() {}));
|
||||
|
||||
stateSub = _pangeaController.myAnalytics.stateStream.listen((_) {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
stateSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<Room> _chats = [];
|
||||
List<Room> get chats {
|
||||
if (_pangeaController.myAnalytics.studentChats.isEmpty) {
|
||||
_pangeaController.myAnalytics.setStudentChats().then((_) {
|
||||
if (_pangeaController.myAnalytics.studentChats.isNotEmpty) {
|
||||
setState(() {});
|
||||
}
|
||||
if (_chats.isEmpty) {
|
||||
_pangeaController.matrixState.client.chatsImAStudentIn.then((result) {
|
||||
setState(() => _chats = result);
|
||||
});
|
||||
}
|
||||
return _pangeaController.myAnalytics.studentChats;
|
||||
return _chats;
|
||||
}
|
||||
|
||||
List<Room> _spaces = [];
|
||||
List<Room> get spaces {
|
||||
if (_pangeaController.myAnalytics.studentSpaces.isEmpty) {
|
||||
_pangeaController.myAnalytics.setStudentSpaces().then((_) {
|
||||
if (_pangeaController.myAnalytics.studentSpaces.isNotEmpty) {
|
||||
setState(() {});
|
||||
}
|
||||
if (_spaces.isEmpty) {
|
||||
_pangeaController.matrixState.client.spaceImAStudentIn.then((result) {
|
||||
setState(() => _spaces = result);
|
||||
});
|
||||
}
|
||||
return _pangeaController.myAnalytics.studentSpaces;
|
||||
return _spaces;
|
||||
}
|
||||
|
||||
String? get userId {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_detection_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/language_detection_model.dart';
|
||||
import 'package:fluffychat/pangea/models/lemma.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
|
|
@ -39,31 +40,29 @@ 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),
|
||||
hasInfo: true,
|
||||
lemmas: [Lemma(form: "This", text: "this", saveVocab: true)],
|
||||
),
|
||||
PangeaToken(
|
||||
text: PangeaTokenText(content: "be", offset: 5, length: 2),
|
||||
hasInfo: true,
|
||||
lemmas: [Lemma(form: "be", text: "be", saveVocab: true)],
|
||||
),
|
||||
PangeaToken(
|
||||
text: PangeaTokenText(content: "a", offset: 8, length: 1),
|
||||
hasInfo: false,
|
||||
lemmas: [],
|
||||
),
|
||||
PangeaToken(
|
||||
text: PangeaTokenText(content: "sample", offset: 10, length: 6),
|
||||
hasInfo: false,
|
||||
lemmas: [],
|
||||
),
|
||||
PangeaToken(
|
||||
text: PangeaTokenText(content: "text", offset: 17, length: 4),
|
||||
hasInfo: false,
|
||||
lemmas: [],
|
||||
),
|
||||
],
|
||||
|
|
@ -89,7 +88,6 @@ class IGCRequestBody {
|
|||
String fullText;
|
||||
String userL1;
|
||||
String userL2;
|
||||
bool tokensOnly;
|
||||
bool enableIT;
|
||||
bool enableIGC;
|
||||
|
||||
|
|
@ -99,7 +97,6 @@ class IGCRequestBody {
|
|||
required this.userL2,
|
||||
required this.enableIGC,
|
||||
required this.enableIT,
|
||||
this.tokensOnly = false,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
|
|
@ -108,6 +105,5 @@ class IGCRequestBody {
|
|||
ModelKey.userL2: userL2,
|
||||
"enable_it": enableIT,
|
||||
"enable_igc": enableIGC,
|
||||
"tokens_only": tokensOnly,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
class PangeaWarningError implements Exception {
|
||||
final String message;
|
||||
PangeaWarningError(message) : message = "Pangea Warning Error: $message";
|
||||
}
|
||||
|
||||
class ErrorHandler {
|
||||
ErrorHandler();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../../config/firebase_options.dart';
|
||||
import '../enum/use_type.dart';
|
||||
|
||||
// PageRoute import
|
||||
|
||||
|
|
@ -90,13 +89,12 @@ class GoogleAnalytics {
|
|||
logEvent('join_group', parameters: {'group_id': classCode});
|
||||
}
|
||||
|
||||
static sendMessage(String chatRoomId, String classCode, UseType useType) {
|
||||
static sendMessage(String chatRoomId, String classCode) {
|
||||
logEvent(
|
||||
'sent_message',
|
||||
parameters: {
|
||||
"chat_id": chatRoomId,
|
||||
'group_id': classCode,
|
||||
"message_type": useType.toString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
|
|
|
|||
62
lib/pangea/utils/inline_tooltip.dart
Normal file
62
lib/pangea/utils/inline_tooltip.dart
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class InlineTooltip extends StatelessWidget {
|
||||
final String body;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const InlineTooltip({
|
||||
super.key,
|
||||
required this.body,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Badge(
|
||||
offset: const Offset(0, -7),
|
||||
backgroundColor: Colors.transparent,
|
||||
label: CircleAvatar(
|
||||
radius: 10,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: const Icon(
|
||||
Icons.close_outlined,
|
||||
size: 15,
|
||||
),
|
||||
onPressed: onClose,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: Theme.of(context).colorScheme.primary.withAlpha(20),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.justify,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
const WidgetSpan(
|
||||
child: Icon(
|
||||
Icons.lightbulb,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const WidgetSpan(
|
||||
child: SizedBox(width: 5),
|
||||
),
|
||||
TextSpan(
|
||||
text: body,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
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';
|
||||
|
||||
|
|
@ -14,17 +15,29 @@ import 'overlay.dart';
|
|||
class InstructionsController {
|
||||
late PangeaController _pangeaController;
|
||||
|
||||
// We have these three methods to make sure that the instructions are not shown too much
|
||||
|
||||
/// Instruction popup was closed by the user
|
||||
final Map<InstructionsEnum, bool> _instructionsClosed = {};
|
||||
|
||||
/// Instruction popup has already been shown this session
|
||||
final Map<InstructionsEnum, bool> _instructionsShown = {};
|
||||
|
||||
/// Returns true if the user requested this popup not be shown again
|
||||
bool? toggledOff(InstructionsEnum key) =>
|
||||
_pangeaController.pStoreService.read(key.toString());
|
||||
|
||||
InstructionsController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
}
|
||||
|
||||
/// Returns true if the instructions were closed
|
||||
/// or turned off by the user via the toggle switch
|
||||
bool wereInstructionsTurnedOff(InstructionsEnum key) =>
|
||||
_pangeaController.pStoreService.read(key.toString()) ??
|
||||
_instructionsClosed[key] ??
|
||||
false;
|
||||
toggledOff(key) ?? _instructionsClosed[key] ?? false;
|
||||
|
||||
void turnOffInstruction(InstructionsEnum key) =>
|
||||
_instructionsClosed[key] = true;
|
||||
|
||||
Future<void> updateEnableInstructions(
|
||||
InstructionsEnum key,
|
||||
|
|
@ -35,7 +48,9 @@ class InstructionsController {
|
|||
value,
|
||||
);
|
||||
|
||||
Future<void> show(
|
||||
/// Instruction Card gives users tips on
|
||||
/// how to use Pangea Chat's features
|
||||
Future<void> showInstructionsPopup(
|
||||
BuildContext context,
|
||||
InstructionsEnum key,
|
||||
String transformTargetKey, [
|
||||
|
|
@ -98,45 +113,35 @@ class InstructionsController {
|
|||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum InstructionsEnum {
|
||||
itInstructions,
|
||||
clickMessage,
|
||||
blurMeansTranslate,
|
||||
tooltipInstructions,
|
||||
}
|
||||
|
||||
extension Copy on InstructionsEnum {
|
||||
String title(BuildContext context) {
|
||||
switch (this) {
|
||||
case InstructionsEnum.itInstructions:
|
||||
return L10n.of(context)!.itInstructionsTitle;
|
||||
case InstructionsEnum.clickMessage:
|
||||
return L10n.of(context)!.clickMessageTitle;
|
||||
case InstructionsEnum.blurMeansTranslate:
|
||||
return L10n.of(context)!.blurMeansTranslateTitle;
|
||||
case InstructionsEnum.tooltipInstructions:
|
||||
return L10n.of(context)!.tooltipInstructionsTitle;
|
||||
/// Returns a widget that will be added to existing widget
|
||||
/// which displays hint text defined in the enum extension
|
||||
Widget getInstructionInlineTooltip(
|
||||
BuildContext context,
|
||||
InstructionsEnum key,
|
||||
VoidCallback onClose,
|
||||
) {
|
||||
if (wereInstructionsTurnedOff(key)) {
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
String body(BuildContext context) {
|
||||
switch (this) {
|
||||
case InstructionsEnum.itInstructions:
|
||||
return L10n.of(context)!.itInstructionsBody;
|
||||
case InstructionsEnum.clickMessage:
|
||||
return L10n.of(context)!.clickMessageBody;
|
||||
case InstructionsEnum.blurMeansTranslate:
|
||||
return L10n.of(context)!.blurMeansTranslateBody;
|
||||
case InstructionsEnum.tooltipInstructions:
|
||||
return PlatformInfos.isMobile
|
||||
? L10n.of(context)!.tooltipInstructionsMobileBody
|
||||
: L10n.of(context)!.tooltipInstructionsBrowserBody;
|
||||
if (L10n.of(context) == null) {
|
||||
ErrorHandler.logError(
|
||||
m: "null context in ITBotButton.showCard",
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return InlineTooltip(
|
||||
body: InstructionsEnum.speechToText.body(context),
|
||||
onClose: onClose,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// User can toggle on to prevent Instruction Card
|
||||
/// from appearing in future sessions
|
||||
class InstructionsToggle extends StatefulWidget {
|
||||
const InstructionsToggle({
|
||||
super.key,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/speech_to_text_models.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/instructions.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/pangea/widgets/common/icon_number_widget.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
|
||||
|
|
@ -65,6 +65,13 @@ class MessageSpeechToTextCardState extends State<MessageSpeechToTextCard> {
|
|||
}
|
||||
}
|
||||
|
||||
void closeHint() {
|
||||
MatrixState.pangeaController.instructions.turnOffInstruction(
|
||||
InstructionsEnum.speechToText,
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
TextSpan _buildTranscriptText(BuildContext context) {
|
||||
final Transcript transcript = speechToTextResponse!.transcript;
|
||||
final List<InlineSpan> spans = [];
|
||||
|
|
@ -172,7 +179,8 @@ class MessageSpeechToTextCardState extends State<MessageSpeechToTextCard> {
|
|||
number:
|
||||
"${selectedToken?.confidence ?? speechToTextResponse!.transcript.confidence}%",
|
||||
toolTip: L10n.of(context)!.accuracy,
|
||||
onPressed: () => MatrixState.pangeaController.instructions.show(
|
||||
onPressed: () => MatrixState.pangeaController.instructions
|
||||
.showInstructionsPopup(
|
||||
context,
|
||||
InstructionsEnum.tooltipInstructions,
|
||||
widget.messageEvent.eventId,
|
||||
|
|
@ -184,7 +192,8 @@ class MessageSpeechToTextCardState extends State<MessageSpeechToTextCard> {
|
|||
number:
|
||||
wordsPerMinuteString != null ? "$wordsPerMinuteString" : "??",
|
||||
toolTip: L10n.of(context)!.wordsPerMinute,
|
||||
onPressed: () => MatrixState.pangeaController.instructions.show(
|
||||
onPressed: () => MatrixState.pangeaController.instructions
|
||||
.showInstructionsPopup(
|
||||
context,
|
||||
InstructionsEnum.tooltipInstructions,
|
||||
widget.messageEvent.eventId,
|
||||
|
|
@ -193,6 +202,11 @@ class MessageSpeechToTextCardState extends State<MessageSpeechToTextCard> {
|
|||
),
|
||||
],
|
||||
),
|
||||
MatrixState.pangeaController.instructions.getInstructionInlineTooltip(
|
||||
context,
|
||||
InstructionsEnum.speechToText,
|
||||
closeHint,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,6 +137,8 @@ class ToolbarDisplayController {
|
|||
? Alignment.bottomLeft
|
||||
: Alignment.topLeft,
|
||||
backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(100),
|
||||
closePrevOverlay:
|
||||
MatrixState.pangeaController.subscriptionController.isSubscribed,
|
||||
);
|
||||
|
||||
if (MatrixState.pAnyState.entries.isNotEmpty) {
|
||||
|
|
|
|||
|
|
@ -29,41 +29,26 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
PangeaRepresentation? repEvent;
|
||||
String? selectionTranslation;
|
||||
String? oldSelectedText;
|
||||
String? l1Code;
|
||||
String? l2Code;
|
||||
bool _fetchingRepresentation = false;
|
||||
|
||||
String? translationLangCode() {
|
||||
if (widget.immersionMode) return l1Code;
|
||||
final String? originalSentCode =
|
||||
widget.messageEvent.originalSent?.content.langCode;
|
||||
return (l1Code == originalSentCode || originalSentCode == null)
|
||||
? l2Code
|
||||
: l1Code;
|
||||
}
|
||||
|
||||
Future<void> fetchRepresentation(BuildContext context) async {
|
||||
final String? langCode = translationLangCode();
|
||||
if (langCode == null) return;
|
||||
if (l1Code == null) return;
|
||||
|
||||
repEvent = widget.messageEvent
|
||||
.representationByLanguage(
|
||||
langCode,
|
||||
l1Code!,
|
||||
)
|
||||
?.content;
|
||||
|
||||
if (repEvent == null && mounted) {
|
||||
repEvent = await widget.messageEvent.representationByLanguageGlobal(
|
||||
langCode: langCode,
|
||||
langCode: l1Code!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> translateSelection() async {
|
||||
final String? targetLang = translationLangCode();
|
||||
|
||||
if (widget.selection.selectedText == null ||
|
||||
targetLang == null ||
|
||||
l1Code == null ||
|
||||
l2Code == null) {
|
||||
selectionTranslation = null;
|
||||
|
|
@ -78,7 +63,7 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
accessToken: accessToken,
|
||||
request: FullTextTranslationRequestModel(
|
||||
text: widget.selection.messageText,
|
||||
tgtLang: translationLangCode()!,
|
||||
tgtLang: l1Code!,
|
||||
userL1: l1Code!,
|
||||
userL2: l2Code!,
|
||||
srcLang: widget.messageEvent.messageDisplayLangCode,
|
||||
|
|
@ -106,11 +91,14 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
}
|
||||
}
|
||||
|
||||
String? get l1Code =>
|
||||
MatrixState.pangeaController.languageController.activeL1Code();
|
||||
String? get l2Code =>
|
||||
MatrixState.pangeaController.languageController.activeL2Code();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
l1Code = MatrixState.pangeaController.languageController.activeL1Code();
|
||||
l2Code = MatrixState.pangeaController.languageController.activeL2Code();
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import 'dart:ui';
|
|||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/instructions.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_context_menu.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -57,20 +57,28 @@ class PangeaRichTextState extends State<PangeaRichText> {
|
|||
}
|
||||
|
||||
void _setTextSpan(String newTextSpan) {
|
||||
widget.toolbarController?.toolbar?.textSelection.setMessageText(
|
||||
newTextSpan,
|
||||
);
|
||||
if (mounted) {
|
||||
try {
|
||||
if (!mounted) return; // Early exit if the widget is no longer in the tree
|
||||
|
||||
widget.toolbarController?.toolbar?.textSelection.setMessageText(
|
||||
newTextSpan,
|
||||
);
|
||||
setState(() {
|
||||
textSpan = newTextSpan;
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
ErrorHandler.logError(
|
||||
e: PangeaWarningError(error),
|
||||
s: stackTrace,
|
||||
m: "Error setting text span in PangeaRichText",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void setTextSpan() {
|
||||
if (_fetchingRepresentation == true) {
|
||||
if (_fetchingRepresentation) {
|
||||
_setTextSpan(
|
||||
textSpan = widget.pangeaMessageEvent.event
|
||||
widget.pangeaMessageEvent.event
|
||||
.getDisplayEvent(widget.pangeaMessageEvent.timeline)
|
||||
.body,
|
||||
);
|
||||
|
|
@ -91,13 +99,17 @@ class PangeaRichTextState extends State<PangeaRichText> {
|
|||
setState(() => _fetchingRepresentation = true);
|
||||
widget.pangeaMessageEvent
|
||||
.representationByLanguageGlobal(
|
||||
langCode: widget.pangeaMessageEvent.messageDisplayLangCode,
|
||||
)
|
||||
.onError(
|
||||
(error, stackTrace) =>
|
||||
ErrorHandler.logError(e: error, s: stackTrace),
|
||||
)
|
||||
.then((event) {
|
||||
langCode: widget.pangeaMessageEvent.messageDisplayLangCode,
|
||||
)
|
||||
.onError((error, stackTrace) {
|
||||
ErrorHandler.logError(
|
||||
e: PangeaWarningError(error),
|
||||
s: stackTrace,
|
||||
m: "Error fetching representation",
|
||||
);
|
||||
return null;
|
||||
}).then((event) {
|
||||
if (!mounted) return;
|
||||
repEvent = event;
|
||||
_setTextSpan(repEvent?.text ?? widget.pangeaMessageEvent.body);
|
||||
}).whenComplete(() {
|
||||
|
|
@ -115,7 +127,7 @@ class PangeaRichTextState extends State<PangeaRichText> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (blur > 0) {
|
||||
pangeaController.instructions.show(
|
||||
pangeaController.instructions.showInstructionsPopup(
|
||||
context,
|
||||
InstructionsEnum.blurMeansTranslate,
|
||||
widget.pangeaMessageEvent.eventId,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import 'package: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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
widget.controller.currentRecordModel;
|
||||
|
||||
bool get isSubmitted =>
|
||||
widget.currentActivity?.userRecord?.record?.latestResponse != null;
|
||||
widget.currentActivity?.userRecord?.record.latestResponse != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -64,15 +64,19 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
.setCurrentModel(widget.currentActivity!.userRecord!.record);
|
||||
selectedChoiceIndex = widget
|
||||
.currentActivity?.practiceActivity.multipleChoice!
|
||||
.choiceIndex(currentRecordModel!.latestResponse!);
|
||||
.choiceIndex(currentRecordModel!.latestResponse!.text!);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void updateChoice(int index) {
|
||||
currentRecordModel?.addResponse(
|
||||
text: widget.controller.currentActivity?.practiceActivity.multipleChoice!
|
||||
text: widget.controller.currentActivity!.practiceActivity.multipleChoice!
|
||||
.choices[index],
|
||||
score: widget.controller.currentActivity!.practiceActivity.multipleChoice!
|
||||
.isCorrect(index)
|
||||
? 1
|
||||
: 0,
|
||||
);
|
||||
setState(() => selectedChoiceIndex = index);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:fluffychat/pangea/constants/language_level_type.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/utils/language_level_copy.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import 'dart:io';
|
|||
import 'package:fcm_shared_isolate/fcm_shared_isolate.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/utils/push_helper.dart';
|
||||
import 'package:fluffychat/widgets/fluffy_chat_app.dart';
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import pasteboard
|
|||
import path_provider_foundation
|
||||
import purchases_flutter
|
||||
import record_darwin
|
||||
import rive_common
|
||||
import sentry_flutter
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
|
|
@ -65,6 +66,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
PurchasesFlutterPlugin.register(with: registry.registrar(forPlugin: "PurchasesFlutterPlugin"))
|
||||
RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin"))
|
||||
RivePlugin.register(with: registry.registrar(forPlugin: "RivePlugin"))
|
||||
SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
|
|
|
|||
|
|
@ -149,33 +149,41 @@ packages:
|
|||
description:
|
||||
name: matcher
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16+1"
|
||||
version: "0.12.16+1"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
version: "0.8.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
||||
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.12.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
version: "1.9.0"
|
||||
pedantic:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
|
@ -242,9 +250,11 @@ packages:
|
|||
description:
|
||||
name: test_api
|
||||
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
||||
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
version: "0.7.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -272,3 +282,5 @@ packages:
|
|||
sdks:
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
#include <pasteboard/pasteboard_plugin.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <record_windows/record_windows_plugin_c_api.h>
|
||||
#include <rive_common/rive_plugin.h>
|
||||
#include <sentry_flutter/sentry_flutter_plugin.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
#include <sqlcipher_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
|
|
@ -40,6 +41,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
||||
RivePluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("RivePlugin"));
|
||||
SentryFlutterPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SentryFlutterPlugin"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||
pasteboard
|
||||
permission_handler_windows
|
||||
record_windows
|
||||
rive_common
|
||||
sentry_flutter
|
||||
share_plus
|
||||
sqlcipher_flutter_libs
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue