Merge branch 'main' into blue-error-handling
This commit is contained in:
commit
a394cece5c
131 changed files with 3267 additions and 56967 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
|
||||
|
|
|
|||
|
|
@ -2894,21 +2894,23 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"helpMeTranslate": "Help me translate!",
|
||||
"helpMeTranslate": "Yes!",
|
||||
"@helpMeTranslate": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"needsItShortMessage": "Try interactive translation!",
|
||||
"needsItShortMessage": "Out of target",
|
||||
"needsIGCShortMessage": "Try interactive grammar assistance!",
|
||||
"@needsItShortMessage": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"needsItMessage": "This message has too many words in your base language.",
|
||||
"needsItMessage": "Wait, that's not {targetLanguage}! Do you need help translating?",
|
||||
"@needsItMessage": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
"placeholders": {
|
||||
"targetLanguage": {}
|
||||
}
|
||||
},
|
||||
"needsIgcMessage": "This message has a grammar error.",
|
||||
"tokenTranslationTitle": "A word is in your base language.",
|
||||
|
|
@ -3109,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!",
|
||||
|
|
@ -3998,6 +4000,10 @@
|
|||
"conversationBotDiscussionZone_discussionTriggerScheduleHourIntervalLabel": "Hours between discussion prompts",
|
||||
"conversationBotDiscussionZone_discussionTriggerReactionEnabledLabel": "Responds on ⏩ reaction",
|
||||
"conversationBotDiscussionZone_discussionTriggerReactionKeyLabel": "Reaction to send discussion prompt",
|
||||
"conversationBotCustomZone_title": "Custom Settings",
|
||||
"conversationBotCustomZone_customSystemPromptLabel": "System prompt",
|
||||
"conversationBotCustomZone_customSystemPromptPlaceholder": "Set custom system prompt",
|
||||
"conversationBotCustomZone_customTriggerReactionEnabledLabel": "Responds on ⏩ reaction",
|
||||
"addConversationBotDialogTitleInvite": "Confirm inviting conversation bot",
|
||||
"addConversationBotButtonInvite": "Invite",
|
||||
"addConversationBotDialogInviteConfirmation": "Invite",
|
||||
|
|
@ -4010,9 +4016,9 @@
|
|||
"wordsPerMinute": "Words per minute",
|
||||
"autoIGCToolName": "Run Language Assistance Automatically",
|
||||
"autoIGCToolDescription": "Automatically run language assistance after typing messages",
|
||||
"runGrammarCorrection": "Run grammar correction",
|
||||
"runGrammarCorrection": "Check message",
|
||||
"grammarCorrectionFailed": "Issues to address",
|
||||
"grammarCorrectionComplete": "Grammar correction complete",
|
||||
"grammarCorrectionComplete": "Looks good!",
|
||||
"leaveRoomDescription": "The chat will be moved to the archive. Other users will be able to see that you have left the chat.",
|
||||
"archiveSpaceDescription": "All chats within this space will be moved to the archive for yourself and other non-admin users.",
|
||||
"leaveSpaceDescription": "All chats within this space will be moved to the archive. Other users will be able to see that you have left the space.",
|
||||
|
|
@ -4058,5 +4064,26 @@
|
|||
"suggestToSpaceDesc": "Suggested spaces will appear in the chat lists for their parent spaces",
|
||||
"practice": "Practice",
|
||||
"noLanguagesSet": "No languages set",
|
||||
"noActivitiesFound": "No practice activities found for this message"
|
||||
"noActivitiesFound": "No practice activities found for this message",
|
||||
"hintTitle": "Hint:",
|
||||
"speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores",
|
||||
"previous": "Previous",
|
||||
"languageButtonLabel": "Language: {currentLanguage}",
|
||||
"@languageButtonLabel": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"currentLanguage": {}
|
||||
}
|
||||
},
|
||||
"interactiveTranslatorAutoPlaySliderHeader": "Autoplay translation",
|
||||
"@interactiveTranslatorAutoPlaySliderHeader": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"interactiveTranslatorAutoPlayDesc": "Launches the interactive translator without asking.",
|
||||
"@interactiveTranslatorAutoPlayDesc": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"changeAnalyticsView": "Change Analytics View"
|
||||
}
|
||||
|
|
@ -3064,6 +3064,17 @@
|
|||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"interactiveTranslatorAutoPlaySliderHeader": "Traducción de reproducción automática",
|
||||
"interactiveTranslatorAutoPlay": "Traductora interactiva de reproducción automática",
|
||||
"@interactiveTranslatorAutoPlay": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"interactiveTranslatorAutoPlayDesc": "Inicia el traductor interactivo sin preguntar.",
|
||||
"@interactiveTranslatorAutoPlayDesc": {
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"grammarAssistance": "Asistencia gramatical",
|
||||
"@grammarAssistance": {
|
||||
"type": "text",
|
||||
|
|
@ -3126,10 +3137,16 @@
|
|||
"translationSeemsFinished": "La traducción parece estar terminada.",
|
||||
"needsItShortMessage": "¡Pruebe la traducción interactiva!",
|
||||
"needsIGCShortMessage": "¡Pruebe el corrector gramatical interactivo!",
|
||||
"needsItMessage": "Este mensaje tiene demasiadas palabras en su idioma base.",
|
||||
"needsItMessage": "Espera, ¡ese no es {targetLanguage}! ¿Necesitas ayuda para traducir?",
|
||||
"@needsItMessage": {
|
||||
"type": "text",
|
||||
"placeholders": {
|
||||
"targetLanguage": {}
|
||||
}
|
||||
},
|
||||
"needsIgcMessage": "Este mensaje tiene un error gramatical.",
|
||||
"tokenTranslationTitle": "Una palabra está en su idioma base.",
|
||||
"helpMeTranslate": "¡Ayúdeme a traducir!",
|
||||
"helpMeTranslate": "¡Sí!",
|
||||
"setToPublicSettingsTitle": "¿Quiere encontrar un compañero de conversación?",
|
||||
"setToPublicSettingsDesc": "Antes de que pueda buscar un compañero de conversación, debe configurar la visibilidad de su perfil como pública.",
|
||||
"publicProfileTitle": "Perfil público",
|
||||
|
|
@ -4512,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",
|
||||
|
|
@ -4609,9 +4626,9 @@
|
|||
"enterNumber": "Introduzca un valor numérico entero.",
|
||||
"autoIGCToolName": "Ejecutar automáticamente la asistencia lingüística",
|
||||
"autoIGCToolDescription": "Ejecutar automáticamente la asistencia lingüística después de escribir mensajes",
|
||||
"runGrammarCorrection": "Corregir la gramática",
|
||||
"runGrammarCorrection": "Comprobar mensaje",
|
||||
"grammarCorrectionFailed": "Cuestiones a tratar",
|
||||
"grammarCorrectionComplete": "Corrección gramatical completa",
|
||||
"grammarCorrectionComplete": "¡Se ve bien!",
|
||||
"leaveRoomDescription": "El chat se moverá al archivo. Los demás usuarios podrán ver que has abandonado el chat.",
|
||||
"archiveSpaceDescription": "Todos los chats de este espacio se moverán al archivo para ti y otros usuarios que no sean administradores.",
|
||||
"leaveSpaceDescription": "Todos los chats dentro de este espacio se moverán al archivo. Los demás usuarios podrán ver que has abandonado el espacio.",
|
||||
|
|
|
|||
BIN
assets/pangea/bot_faces/pangea_bot.riv
Normal file
BIN
assets/pangea/bot_faces/pangea_bot.riv
Normal file
Binary file not shown.
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
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/it_bar.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';
|
||||
|
|
@ -55,9 +54,6 @@ class ChatInputRow extends StatelessWidget {
|
|||
|
||||
return Column(
|
||||
children: [
|
||||
ITBar(
|
||||
choreographer: controller.choreographer,
|
||||
),
|
||||
Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import 'package:future_loading_dialog/future_loading_dialog.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import '../../pangea/choreographer/widgets/it_bar.dart';
|
||||
import '../../utils/stream_extension.dart';
|
||||
import 'chat_emoji_picker.dart';
|
||||
import 'chat_input_row.dart';
|
||||
|
|
@ -440,6 +441,9 @@ class ChatView extends StatelessWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ConnectionStatusHeader(),
|
||||
ITBar(
|
||||
choreographer: controller.choreographer,
|
||||
),
|
||||
ReactionsPicker(controller),
|
||||
ReplyDisplay(controller),
|
||||
ChatInputRow(controller),
|
||||
|
|
|
|||
|
|
@ -470,7 +470,7 @@ class Message extends StatelessWidget {
|
|||
?.showUseType ??
|
||||
false) ...[
|
||||
pangeaMessageEvent!
|
||||
.useType
|
||||
.msgUseType
|
||||
.iconView(
|
||||
context,
|
||||
textColor
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -35,11 +35,26 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||
final powerLevels = Map<String, dynamic>.from(powerLevelsContent)
|
||||
// #Pangea
|
||||
// ..removeWhere((k, v) => v is! int);
|
||||
..removeWhere((k, v) => v is! int || k.equals("m.call.invite"));
|
||||
..removeWhere(
|
||||
(k, v) =>
|
||||
v is! int ||
|
||||
k.equals("m.call.invite") ||
|
||||
k.equals("historical") ||
|
||||
k.equals("state_default"),
|
||||
);
|
||||
// Pangea#
|
||||
final eventsPowerLevels = Map<String, int?>.from(
|
||||
powerLevelsContent.tryGetMap<String, int?>('events') ?? {},
|
||||
)..removeWhere((k, v) => v is! int);
|
||||
// #Pangea
|
||||
)..removeWhere(
|
||||
(k, v) =>
|
||||
v is! int ||
|
||||
k.equals("m.space.child") ||
|
||||
k.equals("pangea.usranalytics") ||
|
||||
k.equals(EventTypes.RoomPowerLevels),
|
||||
);
|
||||
// )..removeWhere((k, v) => v is! int);
|
||||
// Pangea#
|
||||
return Column(
|
||||
children: [
|
||||
Column(
|
||||
|
|
@ -57,51 +72,59 @@ class ChatPermissionsSettingsView extends StatelessWidget {
|
|||
),
|
||||
canEdit: room.canChangePowerLevel,
|
||||
),
|
||||
Divider(color: Theme.of(context).dividerColor),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.notifications,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
const key = 'rooms';
|
||||
final value = powerLevelsContent
|
||||
.containsKey('notifications')
|
||||
? powerLevelsContent
|
||||
.tryGetMap<String, Object?>('notifications')
|
||||
?.tryGet<int>('rooms') ??
|
||||
0
|
||||
: 0;
|
||||
return PermissionsListTile(
|
||||
permissionKey: key,
|
||||
permission: value,
|
||||
category: 'notifications',
|
||||
canEdit: room.canChangePowerLevel,
|
||||
onChanged: (level) => controller.editPowerLevel(
|
||||
context,
|
||||
key,
|
||||
value,
|
||||
newLevel: level,
|
||||
category: 'notifications',
|
||||
// #Pangea
|
||||
// Why would teacher need to stop students from seeing notifications?
|
||||
// Divider(color: Theme.of(context).dividerColor),
|
||||
// ListTile(
|
||||
// title: Text(
|
||||
// L10n.of(context)!.notifications,
|
||||
// style: TextStyle(
|
||||
// color: Theme.of(context).colorScheme.primary,
|
||||
// fontWeight: FontWeight.bold,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// Builder(
|
||||
// builder: (context) {
|
||||
// const key = 'rooms';
|
||||
// final value = powerLevelsContent
|
||||
// .containsKey('notifications')
|
||||
// ? powerLevelsContent
|
||||
// .tryGetMap<String, Object?>('notifications')
|
||||
// ?.tryGet<int>('rooms') ??
|
||||
// 0
|
||||
// : 0;
|
||||
// return PermissionsListTile(
|
||||
// permissionKey: key,
|
||||
// permission: value,
|
||||
// category: 'notifications',
|
||||
// canEdit: room.canChangePowerLevel,
|
||||
// onChanged: (level) => controller.editPowerLevel(
|
||||
// context,
|
||||
// key,
|
||||
// value,
|
||||
// newLevel: level,
|
||||
// category: 'notifications',
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// Only show if there are actually items in this category
|
||||
if (eventsPowerLevels.isNotEmpty)
|
||||
// Pangea#
|
||||
Divider(color: Theme.of(context).dividerColor),
|
||||
// #Pangea
|
||||
if (eventsPowerLevels.isNotEmpty)
|
||||
// Pangea#
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.configureChat,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Divider(color: Theme.of(context).dividerColor),
|
||||
ListTile(
|
||||
title: Text(
|
||||
L10n.of(context)!.configureChat,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final entry in eventsPowerLevels.entries)
|
||||
PermissionsListTile(
|
||||
permissionKey: entry.key,
|
||||
|
|
|
|||
|
|
@ -5,15 +5,16 @@ 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';
|
||||
import 'package:fluffychat/pangea/models/user_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/overlay.dart';
|
||||
|
|
@ -23,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';
|
||||
|
|
@ -93,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() {
|
||||
|
|
@ -165,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;
|
||||
|
||||
|
|
@ -178,6 +174,7 @@ class Choreographer {
|
|||
_textController.setSystemText("", EditType.itStart);
|
||||
}
|
||||
|
||||
/// Handles any changes to the text input
|
||||
_onChangeListener() {
|
||||
if (_noChange) {
|
||||
return;
|
||||
|
|
@ -186,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();
|
||||
|
||||
|
|
@ -210,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
|
||||
|
|
@ -219,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 =
|
||||
|
|
@ -237,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 {
|
||||
|
|
@ -480,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);
|
||||
|
|
@ -521,6 +523,12 @@ class Choreographer {
|
|||
chatController.room,
|
||||
);
|
||||
|
||||
bool get itAutoPlayEnabled =>
|
||||
pangeaController.pStoreService.read(
|
||||
MatrixProfile.itAutoPlay.title,
|
||||
) ??
|
||||
false;
|
||||
|
||||
bool get definitionsEnabled =>
|
||||
pangeaController.permissionsController.isToolEnabled(
|
||||
ToolSetting.definitions,
|
||||
|
|
@ -570,13 +578,3 @@ class Choreographer {
|
|||
return AssistanceState.complete;
|
||||
}
|
||||
}
|
||||
|
||||
// assistance state is, user has not typed a message, user has typed a message and IGC has not run,
|
||||
// IGC is running, IGC has run and there are remaining steps (either IT or IGC), or all steps are done
|
||||
enum AssistanceState {
|
||||
noMessage,
|
||||
notFetched,
|
||||
fetching,
|
||||
fetched,
|
||||
complete,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,6 +152,13 @@ class IgcController {
|
|||
const int firstMatchIndex = 0;
|
||||
final PangeaMatch match = igcTextData!.matches[firstMatchIndex];
|
||||
|
||||
if (match.isITStart &&
|
||||
choreographer.itAutoPlayEnabled &&
|
||||
igcTextData != null) {
|
||||
choreographer.onITStart(igcTextData!.matches[firstMatchIndex]);
|
||||
return;
|
||||
}
|
||||
|
||||
OverlayUtil.showPositionedCard(
|
||||
context: context,
|
||||
cardToShow: SpanCard(
|
||||
|
|
@ -189,7 +178,7 @@ class IgcController {
|
|||
),
|
||||
roomId: choreographer.roomId,
|
||||
),
|
||||
cardSize: match.isITStart ? const Size(350, 220) : const Size(350, 400),
|
||||
cardSize: match.isITStart ? const Size(350, 260) : const Size(350, 400),
|
||||
transformTargetId: choreographer.inputTransformTargetKey,
|
||||
);
|
||||
}
|
||||
|
|
@ -206,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();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class ITController {
|
|||
Choreographer choreographer;
|
||||
|
||||
bool _isOpen = false;
|
||||
bool _willOpen = false;
|
||||
bool _isEditingSourceText = false;
|
||||
bool showChoiceFeedback = false;
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ class ITController {
|
|||
|
||||
void clear() {
|
||||
_isOpen = false;
|
||||
_willOpen = false;
|
||||
showChoiceFeedback = false;
|
||||
_isEditingSourceText = false;
|
||||
|
||||
|
|
@ -54,6 +56,7 @@ class ITController {
|
|||
}
|
||||
|
||||
Future<void> initializeIT(ITStartData itStartData) async {
|
||||
_willOpen = true;
|
||||
Future.delayed(const Duration(microseconds: 100), () {
|
||||
_isOpen = true;
|
||||
});
|
||||
|
|
@ -61,17 +64,14 @@ class ITController {
|
|||
}
|
||||
|
||||
void closeIT() {
|
||||
//if they close it before choosing anything, just put their text back
|
||||
//if they close it before completing, just put their text back
|
||||
//PTODO - explore using last itStep
|
||||
if (choreographer.currentText.isEmpty) {
|
||||
choreographer.textController.text = sourceText ?? "";
|
||||
}
|
||||
choreographer.textController.text = sourceText ?? "";
|
||||
clear();
|
||||
}
|
||||
|
||||
/// 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(
|
||||
|
|
@ -96,21 +96,12 @@ class ITController {
|
|||
request: FullTextTranslationRequestModel(
|
||||
text: _itStartData!.text,
|
||||
tgtLang: choreographer.l1LangCode!,
|
||||
srcLang: choreographer.l2LangCode,
|
||||
srcLang: _itStartData!.langCode,
|
||||
userL1: choreographer.l1LangCode!,
|
||||
userL2: choreographer.l2LangCode!,
|
||||
),
|
||||
);
|
||||
sourceText = res.bestTranslation;
|
||||
// } catch (err, stack) {
|
||||
// debugger(when: kDebugMode);
|
||||
// if (_itStartData?.text.isNotEmpty ?? false) {
|
||||
// ErrorHandler.logError(e: err, s: stack);
|
||||
// sourceText = _itStartData!.text;
|
||||
// } else {
|
||||
// rethrow;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// used 1) at very beginning (with custom input = null)
|
||||
|
|
@ -166,7 +157,7 @@ class ITController {
|
|||
|
||||
if (isTranslationDone) {
|
||||
choreographer.altTranslator.setTranslationFeedback();
|
||||
choreographer.getLanguageHelp(true);
|
||||
choreographer.getLanguageHelp(onlyTokensAndLanguageDetection: true);
|
||||
} else {
|
||||
getNextTranslationData();
|
||||
}
|
||||
|
|
@ -217,31 +208,17 @@ class ITController {
|
|||
|
||||
Future<void> onEditSourceTextSubmit(String newSourceText) async {
|
||||
try {
|
||||
sourceText = newSourceText;
|
||||
_isOpen = true;
|
||||
_isEditingSourceText = false;
|
||||
final String currentText = choreographer.currentText;
|
||||
_itStartData = ITStartData(newSourceText, choreographer.l1LangCode);
|
||||
completedITSteps = [];
|
||||
currentITStep = null;
|
||||
nextITStep = null;
|
||||
goldRouteTracker = GoldRouteTracker.defaultTracker;
|
||||
payLoadIds = [];
|
||||
|
||||
choreographer.startLoading();
|
||||
|
||||
final List<ITResponseModel> responses = await Future.wait([
|
||||
_customInputTranslation(""),
|
||||
_customInputTranslation(choreographer.currentText),
|
||||
]);
|
||||
if (responses[0].goldContinuances != null &&
|
||||
responses[0].goldContinuances!.isNotEmpty) {
|
||||
goldRouteTracker = GoldRouteTracker(
|
||||
responses[0].goldContinuances!,
|
||||
sourceText!,
|
||||
);
|
||||
}
|
||||
currentITStep = CurrentITStep(
|
||||
sourceText: sourceText!,
|
||||
currentText: currentText,
|
||||
responseModel: responses[1],
|
||||
storedGoldContinuances: goldRouteTracker.continuances,
|
||||
);
|
||||
|
||||
_addPayloadId(responses[1]);
|
||||
_setSourceText();
|
||||
getTranslationData(false);
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
if (err is! http.Response) {
|
||||
|
|
@ -252,6 +229,7 @@ class ITController {
|
|||
);
|
||||
} finally {
|
||||
choreographer.stopLoading();
|
||||
choreographer.textController.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -334,15 +312,14 @@ class ITController {
|
|||
|
||||
bool get isOpen => _isOpen;
|
||||
|
||||
bool get willOpen => _willOpen;
|
||||
|
||||
String get targetLangCode => choreographer.l2LangCode!;
|
||||
|
||||
String get sourceLangCode => choreographer.l1LangCode!;
|
||||
|
||||
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';
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
import '../../utils/bot_style.dart';
|
||||
import 'it_shimmer.dart';
|
||||
|
||||
class ChoicesArray extends StatelessWidget {
|
||||
class ChoicesArray extends StatefulWidget {
|
||||
final bool isLoading;
|
||||
final List<Choice>? choices;
|
||||
final void Function(int) onPressed;
|
||||
|
|
@ -32,23 +32,51 @@ class ChoicesArray extends StatelessWidget {
|
|||
this.onLongPress,
|
||||
});
|
||||
|
||||
@override
|
||||
ChoicesArrayState createState() => ChoicesArrayState();
|
||||
}
|
||||
|
||||
class ChoicesArrayState extends State<ChoicesArray> {
|
||||
bool interactionDisabled = false;
|
||||
|
||||
void disableInteraction() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
interactionDisabled = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void enableInteractions() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
interactionDisabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return isLoading && (choices == null || choices!.length <= 1)
|
||||
? ItShimmer(originalSpan: originalSpan)
|
||||
return widget.isLoading &&
|
||||
(widget.choices == null || widget.choices!.length <= 1)
|
||||
? ItShimmer(originalSpan: widget.originalSpan)
|
||||
: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: choices
|
||||
children: widget.choices
|
||||
?.asMap()
|
||||
.entries
|
||||
.map(
|
||||
(entry) => ChoiceItem(
|
||||
theme: theme,
|
||||
onLongPress: isActive ? onLongPress : null,
|
||||
onPressed: isActive ? onPressed : (_) {},
|
||||
onLongPress:
|
||||
widget.isActive ? widget.onLongPress : null,
|
||||
onPressed: widget.isActive ? widget.onPressed : (_) {},
|
||||
entry: entry,
|
||||
isSelected: selectedChoiceIndex == entry.key,
|
||||
interactionDisabled: interactionDisabled,
|
||||
enableInteraction: enableInteractions,
|
||||
disableInteraction: disableInteraction,
|
||||
isSelected: widget.selectedChoiceIndex == entry.key,
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
|
|
@ -77,6 +105,9 @@ class ChoiceItem extends StatelessWidget {
|
|||
required this.onPressed,
|
||||
required this.entry,
|
||||
required this.isSelected,
|
||||
required this.interactionDisabled,
|
||||
required this.enableInteraction,
|
||||
required this.disableInteraction,
|
||||
});
|
||||
|
||||
final MapEntry<int, Choice> entry;
|
||||
|
|
@ -84,6 +115,9 @@ class ChoiceItem extends StatelessWidget {
|
|||
final void Function(int p1)? onLongPress;
|
||||
final void Function(int p1) onPressed;
|
||||
final bool isSelected;
|
||||
final bool interactionDisabled;
|
||||
final VoidCallback enableInteraction;
|
||||
final VoidCallback disableInteraction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -97,6 +131,8 @@ class ChoiceItem extends StatelessWidget {
|
|||
key: ValueKey(entry.value.text),
|
||||
selected: entry.value.color != null,
|
||||
isGold: entry.value.isGold,
|
||||
enableInteraction: enableInteraction,
|
||||
disableInteraction: disableInteraction,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: EdgeInsets.zero,
|
||||
|
|
@ -130,9 +166,11 @@ class ChoiceItem extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
onLongPress:
|
||||
onLongPress != null ? () => onLongPress!(entry.key) : null,
|
||||
onPressed: () => onPressed(entry.key),
|
||||
onLongPress: onLongPress != null && !interactionDisabled
|
||||
? () => onLongPress!(entry.key)
|
||||
: null,
|
||||
onPressed:
|
||||
interactionDisabled ? null : () => onPressed(entry.key),
|
||||
child: Text(
|
||||
entry.value.text,
|
||||
style: BotStyle.text(context),
|
||||
|
|
@ -152,11 +190,15 @@ class ChoiceAnimationWidget extends StatefulWidget {
|
|||
final Widget child;
|
||||
final bool selected;
|
||||
final bool isGold;
|
||||
final VoidCallback enableInteraction;
|
||||
final VoidCallback disableInteraction;
|
||||
|
||||
const ChoiceAnimationWidget({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.selected,
|
||||
required this.enableInteraction,
|
||||
required this.disableInteraction,
|
||||
this.isGold = false,
|
||||
});
|
||||
|
||||
|
|
@ -164,11 +206,13 @@ class ChoiceAnimationWidget extends StatefulWidget {
|
|||
ChoiceAnimationWidgetState createState() => ChoiceAnimationWidgetState();
|
||||
}
|
||||
|
||||
enum AnimationState { ready, forward, reverse, finished }
|
||||
|
||||
class ChoiceAnimationWidgetState extends State<ChoiceAnimationWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _animation;
|
||||
bool animationPlayed = false;
|
||||
AnimationState animationState = AnimationState.ready;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -196,17 +240,29 @@ class ChoiceAnimationWidgetState extends State<ChoiceAnimationWidget>
|
|||
),
|
||||
]).animate(_controller);
|
||||
|
||||
if (widget.selected && !animationPlayed) {
|
||||
widget.enableInteraction();
|
||||
|
||||
if (widget.selected && animationState == AnimationState.ready) {
|
||||
widget.disableInteraction();
|
||||
_controller.forward();
|
||||
animationPlayed = true;
|
||||
setState(() {});
|
||||
setState(() {
|
||||
animationState = AnimationState.forward;
|
||||
});
|
||||
}
|
||||
_controller.addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
if (status == AnimationStatus.completed &&
|
||||
animationState == AnimationState.forward) {
|
||||
_controller.reverse();
|
||||
} else if (status == AnimationStatus.dismissed) {
|
||||
_controller.stop();
|
||||
_controller.reset();
|
||||
setState(() {
|
||||
animationState = AnimationState.reverse;
|
||||
});
|
||||
}
|
||||
if (status == AnimationStatus.dismissed &&
|
||||
animationState == AnimationState.reverse) {
|
||||
widget.enableInteraction();
|
||||
setState(() {
|
||||
animationState = AnimationState.finished;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -214,10 +270,12 @@ class ChoiceAnimationWidgetState extends State<ChoiceAnimationWidget>
|
|||
@override
|
||||
void didUpdateWidget(ChoiceAnimationWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selected && !animationPlayed) {
|
||||
if (widget.selected && animationState == AnimationState.ready) {
|
||||
widget.disableInteraction();
|
||||
_controller.forward();
|
||||
animationPlayed = true;
|
||||
setState(() {});
|
||||
setState(() {
|
||||
animationState = AnimationState.forward;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/widgets/translation_finished_flow.dart';
|
||||
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
|
@ -21,93 +22,107 @@ class ITBar extends StatelessWidget {
|
|||
final Choreographer choreographer;
|
||||
const ITBar({super.key, required this.choreographer});
|
||||
|
||||
ITController get controller => choreographer.itController;
|
||||
ITController get itController => choreographer.itController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!controller.isOpen) return const SizedBox();
|
||||
|
||||
return CompositedTransformTarget(
|
||||
link: choreographer.itBarLinkAndKey.link,
|
||||
child: Container(
|
||||
key: choreographer.itBarLinkAndKey.key,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(AppConfig.borderRadius),
|
||||
topRight: Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(0, 3, 3, 3),
|
||||
child: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
return AnimatedSize(
|
||||
duration: itController.willOpen
|
||||
? const Duration(milliseconds: 2000)
|
||||
: const Duration(milliseconds: 500),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
clipBehavior: Clip.none,
|
||||
child: !itController.willOpen
|
||||
? const SizedBox()
|
||||
: CompositedTransformTarget(
|
||||
link: choreographer.itBarLinkAndKey.link,
|
||||
child: AnimatedOpacity(
|
||||
duration: itController.willOpen
|
||||
? const Duration(milliseconds: 2000)
|
||||
: const Duration(milliseconds: 500),
|
||||
opacity: itController.willOpen ? 1.0 : 0.0,
|
||||
child: Container(
|
||||
key: choreographer.itBarLinkAndKey.key,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).brightness == Brightness.light
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(AppConfig.borderRadius),
|
||||
topRight: Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(0, 3, 3, 3),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// // Row(
|
||||
// // mainAxisAlignment: MainAxisAlignment.start,
|
||||
// // crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// // children: [
|
||||
// // CounterDisplay(
|
||||
// // correct: controller.correctChoices,
|
||||
// // custom: controller.customChoices,
|
||||
// // incorrect: controller.incorrectChoices,
|
||||
// // yellow: controller.wildcardChoices,
|
||||
// // ),
|
||||
// // CompositedTransformTarget(
|
||||
// // link: choreographer.itBotLayerLinkAndKey.link,
|
||||
// // child: ITBotButton(
|
||||
// // key: choreographer.itBotLayerLinkAndKey.key,
|
||||
// // choreographer: choreographer,
|
||||
// // ),
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// ITCloseButton(choreographer: choreographer),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(height: 40.0),
|
||||
OriginalText(controller: controller),
|
||||
const SizedBox(height: 7.0),
|
||||
IntrinsicHeight(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 80),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Center(
|
||||
child: controller.choreographer.errorService.isError
|
||||
? ITError(
|
||||
error: controller
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// // Row(
|
||||
// // mainAxisAlignment: MainAxisAlignment.start,
|
||||
// // crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// // children: [
|
||||
// // CounterDisplay(
|
||||
// // correct: controller.correctChoices,
|
||||
// // custom: controller.customChoices,
|
||||
// // incorrect: controller.incorrectChoices,
|
||||
// // yellow: controller.wildcardChoices,
|
||||
// // ),
|
||||
// // CompositedTransformTarget(
|
||||
// // link: choreographer.itBotLayerLinkAndKey.link,
|
||||
// // child: ITBotButton(
|
||||
// // key: choreographer.itBotLayerLinkAndKey.key,
|
||||
// // choreographer: choreographer,
|
||||
// // ),
|
||||
// // ),
|
||||
// // ],
|
||||
// // ),
|
||||
// ITCloseButton(choreographer: choreographer),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(height: 40.0),
|
||||
OriginalText(controller: itController),
|
||||
const SizedBox(height: 7.0),
|
||||
IntrinsicHeight(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: 80),
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: Center(
|
||||
child: itController.choreographer.errorService.isError
|
||||
? ITError(
|
||||
error: itController
|
||||
.choreographer.errorService.error!,
|
||||
controller: controller,
|
||||
controller: itController,
|
||||
)
|
||||
: controller.showChoiceFeedback
|
||||
? ChoiceFeedbackText(controller: controller)
|
||||
: controller.isTranslationDone
|
||||
? TranslationFeedback(
|
||||
controller: controller,
|
||||
)
|
||||
: ITChoices(controller: controller),
|
||||
),
|
||||
: itController.showChoiceFeedback
|
||||
? ChoiceFeedbackText(controller: itController)
|
||||
: itController.isTranslationDone
|
||||
? TranslationFeedback(
|
||||
controller: itController,
|
||||
)
|
||||
: ITChoices(controller: itController),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0.0,
|
||||
right: 0.0,
|
||||
child: ITCloseButton(choreographer: choreographer),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0.0,
|
||||
right: 0.0,
|
||||
child: ITCloseButton(choreographer: choreographer),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -184,10 +199,23 @@ class OriginalText extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
if (!controller.isEditingSourceText && controller.sourceText != null)
|
||||
IconButton(
|
||||
onPressed: () => controller.setIsEditingSourceText(true),
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
if (
|
||||
!controller.isEditingSourceText
|
||||
&& controller.sourceText != null
|
||||
)
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
opacity: controller.nextITStep != null
|
||||
? 1.0
|
||||
: 0.0,
|
||||
child: IconButton(
|
||||
onPressed: () => {
|
||||
if (controller.nextITStep != null) {
|
||||
controller.setIsEditingSourceText(true),
|
||||
},
|
||||
},
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -45,8 +45,9 @@ class ITBotButton extends StatelessWidget {
|
|||
);
|
||||
|
||||
return IconButton(
|
||||
icon: const BotFace(width: 40.0, expression: BotExpression.right),
|
||||
onPressed: () => choreographer.pangeaController.instructions.show(
|
||||
icon: const BotFace(width: 40.0, expression: BotExpression.idle),
|
||||
onPressed: () =>
|
||||
choreographer.pangeaController.instructions.showInstructionsPopup(
|
||||
context,
|
||||
InstructionsEnum.itInstructions,
|
||||
choreographer.itBotTransformTargetKey,
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ class ITFeedbackCardView extends StatelessWidget {
|
|||
children: [
|
||||
CardHeader(
|
||||
text: controller.widget.req.chosenContinuance,
|
||||
botExpression: BotExpression.down,
|
||||
botExpression: BotExpression.nonGold,
|
||||
),
|
||||
Text(
|
||||
controller.widget.choiceFeedback,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/constants/colors.dart';
|
||||
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/assistance_state_enum.dart';
|
||||
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
|
@ -54,15 +53,15 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
setState(() => prevState = assistanceState);
|
||||
}
|
||||
|
||||
bool get itEnabled => widget.controller.choreographer.itEnabled;
|
||||
bool get igcEnabled => widget.controller.choreographer.igcEnabled;
|
||||
CanSendStatus get canSendStatus =>
|
||||
widget.controller.pangeaController.subscriptionController.canSendStatus;
|
||||
bool get grammarCorrectionEnabled =>
|
||||
(itEnabled || igcEnabled) && canSendStatus == CanSendStatus.subscribed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool itEnabled = widget.controller.choreographer.itEnabled;
|
||||
final bool igcEnabled = widget.controller.choreographer.igcEnabled;
|
||||
final CanSendStatus canSendStatus =
|
||||
widget.controller.pangeaController.subscriptionController.canSendStatus;
|
||||
final bool grammarCorrectionEnabled =
|
||||
(itEnabled || igcEnabled) && canSendStatus == CanSendStatus.subscribed;
|
||||
|
||||
if (!grammarCorrectionEnabled ||
|
||||
widget.controller.choreographer.isAutoIGCEnabled ||
|
||||
widget.controller.choreographer.choreoMode == ChoreoMode.it) {
|
||||
|
|
@ -89,11 +88,11 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
disabledElevation: 0,
|
||||
shape: const CircleBorder(),
|
||||
onPressed: () {
|
||||
if (assistanceState != AssistanceState.complete) {
|
||||
if (assistanceState != AssistanceState.fetching) {
|
||||
widget.controller.choreographer
|
||||
.getLanguageHelp(
|
||||
false,
|
||||
true,
|
||||
onlyTokensAndLanguageDetection: false,
|
||||
manual: true,
|
||||
)
|
||||
.then((_) {
|
||||
if (widget.controller.choreographer.igc.igcTextData != null &&
|
||||
|
|
@ -142,32 +141,3 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension AssistanceStateExtension on AssistanceState {
|
||||
Color stateColor(context) {
|
||||
switch (this) {
|
||||
case AssistanceState.noMessage:
|
||||
case AssistanceState.notFetched:
|
||||
case AssistanceState.fetching:
|
||||
return Theme.of(context).colorScheme.primary;
|
||||
case AssistanceState.fetched:
|
||||
return PangeaColors.igcError;
|
||||
case AssistanceState.complete:
|
||||
return AppConfig.success;
|
||||
}
|
||||
}
|
||||
|
||||
String tooltip(L10n l10n) {
|
||||
switch (this) {
|
||||
case AssistanceState.noMessage:
|
||||
case AssistanceState.notFetched:
|
||||
return l10n.runGrammarCorrection;
|
||||
case AssistanceState.fetching:
|
||||
return "";
|
||||
case AssistanceState.fetched:
|
||||
return l10n.grammarCorrectionFailed;
|
||||
case AssistanceState.complete:
|
||||
return l10n.grammarCorrectionComplete;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ class Environment {
|
|||
DateTime.utc(2023, 1, 25).isBefore(DateTime.now());
|
||||
|
||||
static String get fileName {
|
||||
return ".env";
|
||||
return ".local_choreo.env";
|
||||
}
|
||||
|
||||
static bool get isStaging => synapsURL.contains("staging");
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
class PrefKey {
|
||||
static const lastFetched = 'LAST_FETCHED';
|
||||
static const flags = 'flags';
|
||||
}
|
||||
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';
|
||||
}
|
||||
|
|
@ -11,5 +11,6 @@ class PLocalKey {
|
|||
static const String dismissedPaywall = 'dismissedPaywall';
|
||||
static const String paywallBackoff = 'paywallBackoff';
|
||||
static const String autoPlayMessages = 'autoPlayMessages';
|
||||
static const String itAutoPlay = 'itAutoPlay';
|
||||
static const String messagesSinceUpdate = 'messagesSinceLastUpdate';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -95,17 +94,16 @@ class ModelKey {
|
|||
static const String languageLevel = "difficulty";
|
||||
static const String safetyModeration = "safety_moderation";
|
||||
static const String mode = "mode";
|
||||
static const String custom = "custom";
|
||||
static const String discussionTopic = "discussion_topic";
|
||||
static const String discussionKeywords = "discussion_keywords";
|
||||
static const String discussionTriggerScheduleEnabled =
|
||||
"discussion_trigger_schedule_enabled";
|
||||
static const String discussionTriggerScheduleHourInterval =
|
||||
"discussion_trigger_schedule_hour_interval";
|
||||
static const String discussionTriggerReactionEnabled =
|
||||
"discussion_trigger_reaction_enabled";
|
||||
static const String discussionTriggerReactionKey =
|
||||
"discussion_trigger_reaction_key";
|
||||
static const String customSystemPrompt = "custom_system_prompt";
|
||||
static const String customTriggerReactionEnabled =
|
||||
"custom_trigger_reaction_enabled";
|
||||
static const String customTriggerReactionKey = "custom_trigger_reaction_key";
|
||||
|
||||
static const String prevEventId = "prev_event_id";
|
||||
static const String prevLastUpdated = "prev_last_updated";
|
||||
|
|
|
|||
|
|
@ -26,7 +26,13 @@ class PangeaEventTypes {
|
|||
static const String report = 'm.report';
|
||||
static const textToSpeechRule = "p.rule.text_to_speech";
|
||||
|
||||
static const pangeaActivityRes = "pangea.activity_res";
|
||||
static const acitivtyRequest = "pangea.activity_req";
|
||||
/// A request to the server to generate activities
|
||||
static const activityRequest = "pangea.activity_req";
|
||||
|
||||
/// A practice activity that is related to a message
|
||||
static const pangeaActivity = "pangea.activity_res";
|
||||
|
||||
/// A record of completion of an activity. There
|
||||
/// can be one per user per activity.
|
||||
static const activityRecord = "pangea.activity_completion";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -27,7 +26,7 @@ class PangeaLanguage {
|
|||
|
||||
static Future<void> initialize() async {
|
||||
try {
|
||||
_langList = await _getCahedFlags();
|
||||
_langList = await _getCachedFlags();
|
||||
if (await _shouldFetch || _langList.isEmpty) {
|
||||
_langList = await LanguageRepo.fetchLanguages();
|
||||
|
||||
|
|
@ -77,7 +76,7 @@ class PangeaLanguage {
|
|||
await MyShared.saveJson(PrefKey.flags, flagMap);
|
||||
}
|
||||
|
||||
static Future<List<LanguageModel>> _getCahedFlags() async {
|
||||
static Future<List<LanguageModel>> _getCachedFlags() async {
|
||||
final Map<dynamic, dynamic>? flagsMap =
|
||||
await MyShared.readJson(PrefKey.flags);
|
||||
if (flagsMap == null) {
|
||||
|
|
|
|||
|
|
@ -62,12 +62,13 @@ class AnalyticsController extends BaseController {
|
|||
timeSpan.toString(),
|
||||
local: true,
|
||||
);
|
||||
setState();
|
||||
}
|
||||
|
||||
///////// SPACE ANALYTICS LANGUAGES //////////
|
||||
String get _analyticsSpaceLangKey => "ANALYTICS_SPACE_LANG_KEY";
|
||||
|
||||
LanguageModel get currentAnalyticsSpaceLang {
|
||||
LanguageModel get currentAnalyticsLang {
|
||||
try {
|
||||
final String? str = _pangeaController.pStoreService.read(
|
||||
_analyticsSpaceLangKey,
|
||||
|
|
@ -83,41 +84,43 @@ class AnalyticsController extends BaseController {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> setCurrentAnalyticsSpaceLang(LanguageModel lang) async {
|
||||
Future<void> setCurrentAnalyticsLang(LanguageModel lang) async {
|
||||
await _pangeaController.pStoreService.save(
|
||||
_analyticsSpaceLangKey,
|
||||
lang.langCode,
|
||||
local: true,
|
||||
);
|
||||
setState();
|
||||
}
|
||||
|
||||
/// given an analytics event type and the current analytics language,
|
||||
/// get the last time the user updated their analytics
|
||||
Future<DateTime?> myAnalyticsLastUpdated(String type) async {
|
||||
// given an analytics event type, get the last updated times
|
||||
// for each of the user's analytics rooms and return the most recent
|
||||
// Most Recent instead of the oldest because, for instance:
|
||||
// My last Spanish event was sent 3 days ago.
|
||||
// My last English event was sent 1 day ago.
|
||||
// When I go to check if the cached data is out of date, the cached item was set 2 days ago.
|
||||
// I know there’s new data available because the English update data (the most recent) is after the cache’s creation time.
|
||||
// So, I should update the cache.
|
||||
final List<Room> analyticsRooms = _pangeaController
|
||||
.matrixState.client.allMyAnalyticsRooms
|
||||
.where((room) => room.isAnalyticsRoom)
|
||||
.toList();
|
||||
|
||||
final List<DateTime> lastUpdates = [];
|
||||
final Map<String, DateTime> langCodeLastUpdates = {};
|
||||
for (final Room analyticsRoom in analyticsRooms) {
|
||||
final String? roomLang = analyticsRoom.madeForLang;
|
||||
if (roomLang == null) continue;
|
||||
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
|
||||
type,
|
||||
_pangeaController.matrixState.client.userID!,
|
||||
);
|
||||
if (lastUpdated != null) {
|
||||
lastUpdates.add(lastUpdated);
|
||||
langCodeLastUpdates[roomLang] = lastUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastUpdates.isEmpty) return null;
|
||||
return lastUpdates.reduce(
|
||||
if (langCodeLastUpdates.isEmpty) return null;
|
||||
final String? l2Code =
|
||||
_pangeaController.languageController.userL2?.langCode;
|
||||
if (l2Code != null && langCodeLastUpdates.containsKey(l2Code)) {
|
||||
return langCodeLastUpdates[l2Code];
|
||||
}
|
||||
return langCodeLastUpdates.values.reduce(
|
||||
(check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent,
|
||||
);
|
||||
}
|
||||
|
|
@ -134,7 +137,7 @@ class AnalyticsController extends BaseController {
|
|||
final List<Future<DateTime?>> lastUpdatedFutures = [];
|
||||
for (final student in space.students) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(currentAnalyticsSpaceLang.langCode, student.id);
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
|
||||
if (analyticsRoom == null) continue;
|
||||
lastUpdatedFutures.add(
|
||||
analyticsRoom.analyticsLastUpdated(
|
||||
|
|
@ -177,28 +180,20 @@ class AnalyticsController extends BaseController {
|
|||
|
||||
//////////////////////////// MESSAGE SUMMARY ANALYTICS ////////////////////////////
|
||||
|
||||
/// get all the summary analytics events for the current user
|
||||
/// in the current language's analytics room
|
||||
Future<List<SummaryAnalyticsEvent>> mySummaryAnalytics() async {
|
||||
// gets all the summary analytics events for the user
|
||||
// since the current timespace's cut off date
|
||||
final analyticsRooms =
|
||||
_pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode);
|
||||
if (analyticsRoom == null) return [];
|
||||
|
||||
final List<SummaryAnalyticsEvent> allEvents = [];
|
||||
|
||||
// TODO switch to using list of futures
|
||||
for (final Room analyticsRoom in analyticsRooms) {
|
||||
final List<AnalyticsEvent>? roomEvents =
|
||||
await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
);
|
||||
|
||||
allEvents.addAll(
|
||||
roomEvents?.cast<SummaryAnalyticsEvent>() ?? [],
|
||||
);
|
||||
}
|
||||
return allEvents;
|
||||
final List<AnalyticsEvent>? roomEvents =
|
||||
await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.summaryAnalytics,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
);
|
||||
return roomEvents?.cast<SummaryAnalyticsEvent>() ?? [];
|
||||
}
|
||||
|
||||
Future<List<SummaryAnalyticsEvent>> spaceMemberAnalytics(
|
||||
|
|
@ -216,7 +211,7 @@ class AnalyticsController extends BaseController {
|
|||
final List<SummaryAnalyticsEvent> analyticsEvents = [];
|
||||
for (final student in space.students) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(currentAnalyticsSpaceLang.langCode, student.id);
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
|
||||
|
||||
if (analyticsRoom != null) {
|
||||
final List<AnalyticsEvent>? roomEvents =
|
||||
|
|
@ -261,7 +256,7 @@ class AnalyticsController extends BaseController {
|
|||
(e.defaultSelected.type == defaultSelected.type) &&
|
||||
(e.selected?.id == selected?.id) &&
|
||||
(e.selected?.type == selected?.type) &&
|
||||
(e.langCode == currentAnalyticsSpaceLang.langCode),
|
||||
(e.langCode == currentAnalyticsLang.langCode),
|
||||
);
|
||||
|
||||
if (index != -1) {
|
||||
|
|
@ -289,7 +284,7 @@ class AnalyticsController extends BaseController {
|
|||
chartAnalyticsModel: chartAnalyticsModel,
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
langCode: currentAnalyticsSpaceLang.langCode,
|
||||
langCode: currentAnalyticsLang.langCode,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -525,20 +520,18 @@ class AnalyticsController extends BaseController {
|
|||
//////////////////////////// CONSTRUCTS ////////////////////////////
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> allMyConstructs() async {
|
||||
final List<Room> analyticsRooms =
|
||||
_pangeaController.matrixState.client.allMyAnalyticsRooms;
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode);
|
||||
if (analyticsRoom == null) return [];
|
||||
|
||||
final List<ConstructAnalyticsEvent> allConstructs = [];
|
||||
for (final Room analyticsRoom in analyticsRooms) {
|
||||
final List<ConstructAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.construct,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
))
|
||||
?.cast<ConstructAnalyticsEvent>();
|
||||
allConstructs.addAll(roomEvents ?? []);
|
||||
}
|
||||
final List<ConstructAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
type: PangeaEventTypes.construct,
|
||||
since: currentAnalyticsTimeSpan.cutOffDate,
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
))
|
||||
?.cast<ConstructAnalyticsEvent>();
|
||||
final List<ConstructAnalyticsEvent> allConstructs = roomEvents ?? [];
|
||||
|
||||
final List<String> adminSpaceRooms =
|
||||
await _pangeaController.matrixState.client.teacherRoomIds;
|
||||
|
|
@ -561,7 +554,7 @@ class AnalyticsController extends BaseController {
|
|||
final List<ConstructAnalyticsEvent> constructEvents = [];
|
||||
for (final student in space.students) {
|
||||
final Room? analyticsRoom = _pangeaController.matrixState.client
|
||||
.analyticsRoomLocal(currentAnalyticsSpaceLang.langCode, student.id);
|
||||
.analyticsRoomLocal(currentAnalyticsLang.langCode, student.id);
|
||||
if (analyticsRoom != null) {
|
||||
final List<ConstructAnalyticsEvent>? roomEvents =
|
||||
(await analyticsRoom.getAnalyticsEvents(
|
||||
|
|
@ -648,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,
|
||||
|
|
@ -661,7 +654,7 @@ class AnalyticsController extends BaseController {
|
|||
e.defaultSelected.type == defaultSelected.type &&
|
||||
e.selected?.id == selected?.id &&
|
||||
e.selected?.type == selected?.type &&
|
||||
e.langCode == currentAnalyticsSpaceLang.langCode,
|
||||
e.langCode == currentAnalyticsLang.langCode,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
|
|
@ -676,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,14 +680,14 @@ class AnalyticsController extends BaseController {
|
|||
events: List.from(events),
|
||||
defaultSelected: defaultSelected,
|
||||
selected: selected,
|
||||
langCode: currentAnalyticsSpaceLang.langCode,
|
||||
langCode: currentAnalyticsLang.langCode,
|
||||
);
|
||||
_cachedConstructs.add(entry);
|
||||
}
|
||||
|
||||
Future<List<ConstructAnalyticsEvent>> getMyConstructs({
|
||||
required AnalyticsSelected defaultSelected,
|
||||
required ConstructType constructType,
|
||||
required ConstructTypeEnum constructType,
|
||||
AnalyticsSelected? selected,
|
||||
}) async {
|
||||
final List<ConstructAnalyticsEvent> unfilteredConstructs =
|
||||
|
|
@ -713,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,
|
||||
|
|
@ -775,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,
|
||||
|
|
@ -905,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,53 +15,72 @@ 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
|
||||
final Duration _timeSinceUpdate = const Duration(days: 1);
|
||||
|
||||
MyAnalyticsController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
}
|
||||
|
||||
// adds the listener that handles when to run automatic updates
|
||||
// to analytics - either after a certain number of messages sent/
|
||||
// received or after a certain amount of time without an update
|
||||
Future<void> addEventsListener() async {
|
||||
final Client client = _pangeaController.matrixState.client;
|
||||
/// adds the listener that handles when to run automatic updates
|
||||
/// to analytics - either after a certain number of messages sent
|
||||
/// received or after a certain amount of time [_timeSinceUpdate] without an update
|
||||
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(const Duration(days: 1));
|
||||
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
|
||||
// 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
|
||||
/// 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
|
||||
|
|
@ -84,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") &&
|
||||
|
|
@ -160,6 +177,7 @@ class MyAnalyticsController extends BaseController {
|
|||
_updateCompleter = Completer<void>();
|
||||
try {
|
||||
await _updateAnalytics();
|
||||
clearMessagesSinceUpdate();
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
|
|
@ -172,182 +190,135 @@ class MyAnalyticsController extends BaseController {
|
|||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// top level analytics sending function. Send analytics
|
||||
// for each type of analytics event
|
||||
// to each of the applicable analytics rooms
|
||||
clearMessagesSinceUpdate();
|
||||
// analytics room for the user and current target language
|
||||
final Room analyticsRoom = await _client.getMyAnalyticsRoom(userL2!);
|
||||
|
||||
// fetch a list of all the chats that the user is studying
|
||||
// and a list of all the spaces in which the user is studying
|
||||
await setStudentChats();
|
||||
await setStudentSpaces();
|
||||
|
||||
// get the last updated time for each analytics room
|
||||
// and the least recent update, which will be used to determine
|
||||
// how far to go back in the chat history to get messages
|
||||
final Map<String, DateTime?> lastUpdatedMap = await _pangeaController
|
||||
.matrixState.client
|
||||
.allAnalyticsRoomsLastUpdated();
|
||||
final List<DateTime> lastUpdates = lastUpdatedMap.values
|
||||
.where((lastUpdate) => lastUpdate != null)
|
||||
.cast<DateTime>()
|
||||
.toList();
|
||||
lastUpdates.sort((a, b) => a.compareTo(b));
|
||||
final DateTime? leastRecentUpdate =
|
||||
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,
|
||||
leastRecentUpdate,
|
||||
// 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] ?? leastRecentUpdate;
|
||||
|
||||
// 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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class PracticeGenerationController {
|
|||
final Event? activityEvent = await pangeaMessageEvent.room.sendPangeaEvent(
|
||||
content: model.toJson(),
|
||||
parentEventId: pangeaMessageEvent.eventId,
|
||||
type: PangeaEventTypes.pangeaActivityRes,
|
||||
type: PangeaEventTypes.pangeaActivity,
|
||||
);
|
||||
|
||||
if (activityEvent == null) {
|
||||
|
|
@ -88,7 +88,7 @@ class PracticeGenerationController {
|
|||
PracticeActivityModel dummyModel(PangeaMessageEvent event) =>
|
||||
PracticeActivityModel(
|
||||
tgtConstructs: [
|
||||
ConstructIdentifier(lemma: "be", type: ConstructType.vocab),
|
||||
ConstructIdentifier(lemma: "be", type: ConstructTypeEnum.vocab),
|
||||
],
|
||||
activityType: ActivityTypeEnum.multipleChoice,
|
||||
langCode: event.messageDisplayLangCode,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -123,6 +123,7 @@ class UserController extends BaseController {
|
|||
: null;
|
||||
|
||||
final bool? autoPlay = migratedProfileInfo(MatrixProfile.autoPlayMessages);
|
||||
final bool? itAutoPlay = migratedProfileInfo(MatrixProfile.itAutoPlay);
|
||||
final bool? trial = migratedProfileInfo(MatrixProfile.activatedFreeTrial);
|
||||
final bool? interactiveTranslator =
|
||||
migratedProfileInfo(MatrixProfile.interactiveTranslator);
|
||||
|
|
@ -142,6 +143,7 @@ class UserController extends BaseController {
|
|||
await updateMatrixProfile(
|
||||
dateOfBirth: dob,
|
||||
autoPlayMessages: autoPlay,
|
||||
itAutoPlay: itAutoPlay,
|
||||
activatedFreeTrial: trial,
|
||||
interactiveTranslator: interactiveTranslator,
|
||||
interactiveGrammar: interactiveGrammar,
|
||||
|
|
@ -223,6 +225,7 @@ class UserController extends BaseController {
|
|||
Future<void> updateMatrixProfile({
|
||||
String? dateOfBirth,
|
||||
bool? autoPlayMessages,
|
||||
bool? itAutoPlay,
|
||||
bool? activatedFreeTrial,
|
||||
bool? interactiveTranslator,
|
||||
bool? interactiveGrammar,
|
||||
|
|
@ -251,6 +254,12 @@ class UserController extends BaseController {
|
|||
autoPlayMessages,
|
||||
);
|
||||
}
|
||||
if (itAutoPlay != null) {
|
||||
await _pangeaController.pStoreService.save(
|
||||
MatrixProfile.itAutoPlay.title,
|
||||
itAutoPlay,
|
||||
);
|
||||
}
|
||||
if (activatedFreeTrial != null) {
|
||||
await _pangeaController.pStoreService.save(
|
||||
MatrixProfile.activatedFreeTrial.title,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
43
lib/pangea/enum/assistance_state_enum.dart
Normal file
43
lib/pangea/enum/assistance_state_enum.dart
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// assistance state is, user has not typed a message, user has typed a message and IGC has not run,
|
||||
// IGC is running, IGC has run and there are remaining steps (either IT or IGC), or all steps are done
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/constants/colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
enum AssistanceState {
|
||||
noMessage,
|
||||
notFetched,
|
||||
fetching,
|
||||
fetched,
|
||||
complete,
|
||||
}
|
||||
|
||||
extension AssistanceStateExtension on AssistanceState {
|
||||
Color stateColor(context) {
|
||||
switch (this) {
|
||||
case AssistanceState.noMessage:
|
||||
case AssistanceState.notFetched:
|
||||
case AssistanceState.fetching:
|
||||
return Theme.of(context).colorScheme.primary;
|
||||
case AssistanceState.fetched:
|
||||
return PangeaColors.igcError;
|
||||
case AssistanceState.complete:
|
||||
return AppConfig.success;
|
||||
}
|
||||
}
|
||||
|
||||
String tooltip(L10n l10n) {
|
||||
switch (this) {
|
||||
case AssistanceState.noMessage:
|
||||
case AssistanceState.notFetched:
|
||||
return l10n.runGrammarCorrection;
|
||||
case AssistanceState.fetching:
|
||||
return "";
|
||||
case AssistanceState.fetched:
|
||||
return l10n.grammarCorrectionFailed;
|
||||
case AssistanceState.complete:
|
||||
return l10n.grammarCorrectionComplete;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,5 +1,5 @@
|
|||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
enum SpanDataTypeEnum {
|
||||
|
|
@ -32,7 +32,10 @@ extension SpanDataTypeEnumExt on SpanDataTypeEnum {
|
|||
case SpanDataTypeEnum.correction:
|
||||
return L10n.of(context)!.correctionDefaultPrompt;
|
||||
case SpanDataTypeEnum.itStart:
|
||||
return L10n.of(context)!.needsItMessage;
|
||||
return L10n.of(context)!.needsItMessage(
|
||||
MatrixState.pangeaController.languageController.userL2?.displayName ??
|
||||
L10n.of(context)!.targetLanguage,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ extension PangeaEvent on Event {
|
|||
return PangeaRepresentation.fromJson(json) as V;
|
||||
case PangeaEventTypes.choreoRecord:
|
||||
return ChoreoRecord.fromJson(json) as V;
|
||||
case PangeaEventTypes.pangeaActivityRes:
|
||||
case PangeaEventTypes.pangeaActivity:
|
||||
return PracticeActivityModel.fromJson(json) as V;
|
||||
case PangeaEventTypes.activityRecord:
|
||||
return PracticeActivityRecordModel.fromJson(json) as V;
|
||||
|
|
|
|||
|
|
@ -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,14 +4,18 @@ 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_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';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_name.dart';
|
||||
|
|
@ -30,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';
|
||||
|
|
@ -129,6 +132,9 @@ extension PangeaRoom on Room {
|
|||
|
||||
Event? get pangeaRoomRulesStateEvent => _pangeaRoomRulesStateEvent;
|
||||
|
||||
Future<List<LanguageModel>> targetLanguages() async =>
|
||||
await _targetLanguages();
|
||||
|
||||
// events
|
||||
|
||||
Future<bool> leaveIfFull() async => await _leaveIfFull();
|
||||
|
|
@ -174,7 +180,6 @@ extension PangeaRoom on Room {
|
|||
PangeaMessageTokens? tokensSent,
|
||||
PangeaMessageTokens? tokensWritten,
|
||||
ChoreoRecord? choreo,
|
||||
UseType? useType,
|
||||
}) =>
|
||||
_pangeaSendTextEvent(
|
||||
message,
|
||||
|
|
@ -191,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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,34 @@ extension SpaceRoomExtension on Room {
|
|||
return null;
|
||||
}
|
||||
|
||||
Future<List<LanguageModel>> _targetLanguages() async {
|
||||
await requestParticipants();
|
||||
final students = _students;
|
||||
|
||||
final Map<LanguageModel, int> langCounts = {};
|
||||
final List<Room> allRooms = client.rooms;
|
||||
for (final User student in students) {
|
||||
for (final Room room in allRooms) {
|
||||
if (!room.isAnalyticsRoomOfUser(student.id)) continue;
|
||||
final String? langCode = room.madeForLang;
|
||||
if (langCode == null ||
|
||||
langCode.isEmpty ||
|
||||
langCode == LanguageKeys.unknownLanguage) {
|
||||
continue;
|
||||
}
|
||||
final LanguageModel lang = PangeaLanguage.byLangCode(langCode);
|
||||
langCounts[lang] ??= 0;
|
||||
langCounts[lang] = langCounts[lang]! + 1;
|
||||
}
|
||||
}
|
||||
// get a list of language models, sorted
|
||||
// by the number of students who are learning that language
|
||||
return langCounts.entries.map((entry) => entry.key).toList()
|
||||
..sort(
|
||||
(a, b) => langCounts[b]!.compareTo(langCounts[a]!),
|
||||
);
|
||||
}
|
||||
|
||||
// DateTime? get _languageSettingsUpdatedAt {
|
||||
// if (!isSpace) return null;
|
||||
// return languageSettingsStateEvent?.originServerTs ?? creationTime;
|
||||
|
|
|
|||
|
|
@ -2,14 +2,20 @@ import 'dart:convert';
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/lemma.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/space_model.dart';
|
||||
import 'package:fluffychat/pangea/models/speech_to_text_models.dart';
|
||||
|
|
@ -22,7 +28,7 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../../widgets/matrix.dart';
|
||||
import '../constants/language_keys.dart';
|
||||
import '../constants/language_constants.dart';
|
||||
import '../constants/pangea_event_types.dart';
|
||||
import '../enum/use_type.dart';
|
||||
import '../utils/error_handler.dart';
|
||||
|
|
@ -31,7 +37,6 @@ class PangeaMessageEvent {
|
|||
late Event _event;
|
||||
final Timeline timeline;
|
||||
final bool ownMessage;
|
||||
bool _isValidPangeaMessageEvent = true;
|
||||
|
||||
PangeaMessageEvent({
|
||||
required Event event,
|
||||
|
|
@ -39,7 +44,7 @@ class PangeaMessageEvent {
|
|||
required this.ownMessage,
|
||||
}) {
|
||||
if (event.type != EventTypes.Message) {
|
||||
_isValidPangeaMessageEvent = false;
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "${event.type} should not be used to make a PangeaMessageEvent",
|
||||
);
|
||||
|
|
@ -542,7 +547,18 @@ class PangeaMessageEvent {
|
|||
originalWritten: false,
|
||||
);
|
||||
|
||||
UseType get useType => useTypeCalculator(originalSent?.choreo);
|
||||
UseType get msgUseType {
|
||||
final ChoreoRecord? choreoRecord = originalSent?.choreo;
|
||||
if (choreoRecord == null) {
|
||||
return UseType.un;
|
||||
} else if (choreoRecord.includedIT) {
|
||||
return UseType.ta;
|
||||
} else if (choreoRecord.hasAcceptedMatches) {
|
||||
return UseType.ga;
|
||||
} else {
|
||||
return UseType.wa;
|
||||
}
|
||||
}
|
||||
|
||||
bool get showUseType =>
|
||||
!ownMessage &&
|
||||
|
|
@ -566,18 +582,8 @@ class PangeaMessageEvent {
|
|||
/// If any activity is not complete, it returns true, indicating that the activity icon should be shown.
|
||||
/// Otherwise, it returns false.
|
||||
bool get hasUncompletedActivity {
|
||||
if (l2Code == null) return false;
|
||||
final List<PracticeActivityEvent> activities = practiceActivities(l2Code!);
|
||||
if (activities.isEmpty) return false;
|
||||
|
||||
// for now, only show the button if the event has no completed activities
|
||||
// TODO - revert this after adding logic to show next activity
|
||||
for (final activity in activities) {
|
||||
if (activity.isComplete) return false;
|
||||
}
|
||||
return true;
|
||||
// if (activities.isEmpty) return false;
|
||||
// return !activities.every((activity) => activity.isComplete);
|
||||
if (practiceActivities.isEmpty) return false;
|
||||
return practiceActivities.any((activity) => !(activity.isComplete));
|
||||
}
|
||||
|
||||
String? get l2Code =>
|
||||
|
|
@ -611,34 +617,36 @@ class PangeaMessageEvent {
|
|||
return steps;
|
||||
}
|
||||
|
||||
List<PracticeActivityEvent> get _practiceActivityEvents => _latestEdit
|
||||
.aggregatedEvents(
|
||||
timeline,
|
||||
PangeaEventTypes.pangeaActivityRes,
|
||||
)
|
||||
.map(
|
||||
(e) => PracticeActivityEvent(
|
||||
timeline: timeline,
|
||||
event: e,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
/// Returns a list of all [PracticeActivityEvent] objects
|
||||
/// associated with this message event.
|
||||
List<PracticeActivityEvent> get _practiceActivityEvents {
|
||||
return _latestEdit
|
||||
.aggregatedEvents(
|
||||
timeline,
|
||||
PangeaEventTypes.pangeaActivity,
|
||||
)
|
||||
.map(
|
||||
(e) => PracticeActivityEvent(
|
||||
timeline: timeline,
|
||||
event: e,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Returns a boolean value indicating whether there are any
|
||||
/// activities associated with this message event for the user's active l2
|
||||
bool get hasActivities {
|
||||
try {
|
||||
final String? l2code =
|
||||
MatrixState.pangeaController.languageController.activeL2Code();
|
||||
|
||||
if (l2code == null) return false;
|
||||
|
||||
return practiceActivities(l2code).isNotEmpty;
|
||||
return practiceActivities.isNotEmpty;
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
List<PracticeActivityEvent> practiceActivities(
|
||||
/// Returns a list of [PracticeActivityEvent] objects for the given [langCode].
|
||||
List<PracticeActivityEvent> practiceActivitiesByLangCode(
|
||||
String langCode, {
|
||||
bool debug = false,
|
||||
}) {
|
||||
|
|
@ -658,14 +666,170 @@ class PangeaMessageEvent {
|
|||
}
|
||||
}
|
||||
|
||||
// List<SpanData> get activities =>
|
||||
//each match is turned into an activity that other students can access
|
||||
//they're not told the answer but have to find it themselves
|
||||
//the message has a blank piece which they fill in themselves
|
||||
/// Returns a list of [PracticeActivityEvent] for the user's active l2.
|
||||
List<PracticeActivityEvent> get practiceActivities =>
|
||||
l2Code == null ? [] : practiceActivitiesByLangCode(l2Code!);
|
||||
|
||||
// replication of logic from message_content.dart
|
||||
// bool get isHtml =>
|
||||
// AppConfig.renderHtml && !_event.redacted && _event.isRichMessage;
|
||||
/// all construct uses for the message, including vocab and grammar
|
||||
List<OneConstructUse> get allConstructUses =>
|
||||
[..._grammarConstructUses, ..._vocabUses, ..._itStepsToConstructUses];
|
||||
|
||||
/// Returns a list of [OneConstructUse] from itSteps for which the continuance
|
||||
/// was selected or ignored. Correct selections are considered in the tokens
|
||||
/// flow. Once all continuances have lemmas, we can do both correct and incorrect
|
||||
/// in this flow. It actually doesn't do anything at all right now, because the
|
||||
/// choregrapher is not returning lemmas for continuances. This is a TODO.
|
||||
/// So currently only the lemmas can be gotten from the tokens for choices that
|
||||
/// are actually in the final message.
|
||||
List<OneConstructUse> get _itStepsToConstructUses {
|
||||
final List<OneConstructUse> uses = [];
|
||||
if (originalSent?.choreo == null) return uses;
|
||||
|
||||
for (final itStep in originalSent!.choreo!.itSteps) {
|
||||
for (final continuance in itStep.continuances) {
|
||||
// this seems to always be false for continuances right now
|
||||
|
||||
if (originalSent!.choreo!.finalMessage.contains(continuance.text)) {
|
||||
continue;
|
||||
}
|
||||
if (continuance.wasClicked) {
|
||||
//PTODO - account for end of flow score
|
||||
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
|
||||
uses.addAll(
|
||||
_lemmasToVocabUses(
|
||||
continuance.lemmas,
|
||||
ConstructUseTypeEnum.incIt,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
|
||||
uses.addAll(
|
||||
_lemmasToVocabUses(
|
||||
continuance.lemmas,
|
||||
ConstructUseTypeEnum.ignIt,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
/// get construct uses of type vocab for the message
|
||||
List<OneConstructUse> get _vocabUses {
|
||||
final List<OneConstructUse> uses = [];
|
||||
|
||||
// missing vital info so return
|
||||
if (event.roomId == null || originalSent?.tokens == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return uses;
|
||||
}
|
||||
|
||||
// for each token, record whether selected in ga, ta, or wa
|
||||
for (final token in originalSent!.tokens!) {
|
||||
uses.addAll(_getVocabUseForToken(token));
|
||||
}
|
||||
|
||||
return uses;
|
||||
}
|
||||
|
||||
/// Returns a list of [OneConstructUse] objects for the given [token]
|
||||
/// If there is no [originalSent] or [originalSent.choreo], the [token] is
|
||||
/// considered to be a [ConstructUseTypeEnum.wa] as long as it matches the target language.
|
||||
/// Later on, we may want to consider putting it in some category of like 'pending'
|
||||
/// If the [token] is in the [originalSent.choreo.acceptedOrIgnoredMatch],
|
||||
/// it is considered to be a [ConstructUseTypeEnum.ga].
|
||||
/// If the [token] is in the [originalSent.choreo.acceptedOrIgnoredMatch.choices],
|
||||
/// it is considered to be a [ConstructUseTypeEnum.corIt].
|
||||
/// If the [token] is not included in any choreoStep, it is considered to be a [ConstructUseTypeEnum.wa].
|
||||
List<OneConstructUse> _getVocabUseForToken(PangeaToken token) {
|
||||
if (originalSent?.choreo == null) {
|
||||
final bool inUserL2 = originalSent?.langCode == l2Code;
|
||||
return _lemmasToVocabUses(
|
||||
token.lemmas,
|
||||
inUserL2 ? ConstructUseTypeEnum.wa : ConstructUseTypeEnum.unk,
|
||||
);
|
||||
}
|
||||
|
||||
for (final step in originalSent!.choreo!.choreoSteps) {
|
||||
/// if 1) accepted match 2) token is in the replacement and 3) replacement
|
||||
/// is in the overall step text, then token was a ga
|
||||
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted &&
|
||||
(step.acceptedOrIgnoredMatch!.match.choices?.any(
|
||||
(r) =>
|
||||
r.value.contains(token.text.content) &&
|
||||
step.text.contains(r.value),
|
||||
) ??
|
||||
false)) {
|
||||
return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.ga);
|
||||
}
|
||||
if (step.itStep != null) {
|
||||
final bool pickedThroughIT =
|
||||
step.itStep!.chosenContinuance?.text.contains(token.text.content) ??
|
||||
false;
|
||||
if (pickedThroughIT) {
|
||||
return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.corIt);
|
||||
//PTODO - check if added via custom input in IT flow
|
||||
}
|
||||
}
|
||||
}
|
||||
return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.wa);
|
||||
}
|
||||
|
||||
/// Convert a list of [lemmas] into a list of vocab uses
|
||||
/// with the given [type]
|
||||
List<OneConstructUse> _lemmasToVocabUses(
|
||||
List<Lemma> lemmas,
|
||||
ConstructUseTypeEnum type,
|
||||
) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
for (final lemma in lemmas) {
|
||||
if (lemma.saveVocab) {
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
useType: type,
|
||||
chatId: event.roomId!,
|
||||
timeStamp: event.originServerTs,
|
||||
lemma: lemma.text,
|
||||
form: lemma.form,
|
||||
msgId: event.eventId,
|
||||
constructType: ConstructTypeEnum.vocab,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
/// get construct uses of type grammar for the message
|
||||
List<OneConstructUse> get _grammarConstructUses {
|
||||
final List<OneConstructUse> uses = [];
|
||||
|
||||
if (originalSent?.choreo == null || event.roomId == null) return uses;
|
||||
|
||||
for (final step in originalSent!.choreo!.choreoSteps) {
|
||||
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) {
|
||||
final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ??
|
||||
step.acceptedOrIgnoredMatch!.match.shortMessage ??
|
||||
step.acceptedOrIgnoredMatch!.match.type.typeName.name;
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
useType: ConstructUseTypeEnum.ga,
|
||||
chatId: event.roomId!,
|
||||
timeStamp: event.originServerTs,
|
||||
lemma: name,
|
||||
form: name,
|
||||
msgId: event.eventId,
|
||||
constructType: ConstructTypeEnum.grammar,
|
||||
id: "${event.eventId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
}
|
||||
|
||||
class URLFinder {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -27,7 +27,7 @@ class PracticeActivityEvent {
|
|||
_content = content;
|
||||
}
|
||||
}
|
||||
if (event.type != PangeaEventTypes.pangeaActivityRes) {
|
||||
if (event.type != PangeaEventTypes.pangeaActivity) {
|
||||
throw Exception(
|
||||
"${event.type} should not be used to make a PracticeActivityEvent",
|
||||
);
|
||||
|
|
@ -39,7 +39,7 @@ class PracticeActivityEvent {
|
|||
return _content!;
|
||||
}
|
||||
|
||||
//in aggregatedEvents for the event, find all practiceActivityRecordEvents whose sender matches the client's userId
|
||||
/// All completion records assosiated with this activity
|
||||
List<PracticeActivityRecordEvent> get allRecords {
|
||||
if (timeline == null) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -54,14 +54,26 @@ class PracticeActivityEvent {
|
|||
.toList();
|
||||
}
|
||||
|
||||
List<PracticeActivityRecordEvent> get userRecords => allRecords
|
||||
.where(
|
||||
(recordEvent) =>
|
||||
recordEvent.event.senderId == recordEvent.event.room.client.userID,
|
||||
)
|
||||
.toList();
|
||||
/// Completion record assosiated with this activity
|
||||
/// for the logged in user, null if there is none
|
||||
PracticeActivityRecordEvent? get userRecord {
|
||||
final List<PracticeActivityRecordEvent> records = allRecords
|
||||
.where(
|
||||
(recordEvent) =>
|
||||
recordEvent.event.senderId ==
|
||||
recordEvent.event.room.client.userID,
|
||||
)
|
||||
.toList();
|
||||
if (records.length > 1) {
|
||||
debugPrint("There should only be one record per user per activity");
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
return records.firstOrNull;
|
||||
}
|
||||
|
||||
String get parentMessageId => event.relationshipEventId!;
|
||||
|
||||
/// Checks if there are any user records in the list for this activity,
|
||||
/// and, if so, then the activity is complete
|
||||
bool get isComplete => userRecords.isNotEmpty;
|
||||
bool get isComplete => userRecord != null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,44 +13,66 @@ class BotOptionsModel {
|
|||
List<String> keywords;
|
||||
bool safetyModeration;
|
||||
String mode;
|
||||
String? custom;
|
||||
String? discussionTopic;
|
||||
String? discussionKeywords;
|
||||
bool? discussionTriggerScheduleEnabled;
|
||||
int? discussionTriggerScheduleHourInterval;
|
||||
bool? discussionTriggerReactionEnabled;
|
||||
String? discussionTriggerReactionKey;
|
||||
String? customSystemPrompt;
|
||||
bool? customTriggerReactionEnabled;
|
||||
String? customTriggerReactionKey;
|
||||
|
||||
BotOptionsModel({
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// General Bot Options
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
this.languageLevel,
|
||||
this.topic = "General Conversation",
|
||||
this.keywords = const [],
|
||||
this.safetyModeration = true,
|
||||
this.mode = "discussion",
|
||||
this.custom = "",
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Discussion Mode Options
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
this.discussionTopic,
|
||||
this.discussionKeywords,
|
||||
this.discussionTriggerScheduleEnabled,
|
||||
this.discussionTriggerScheduleHourInterval,
|
||||
this.discussionTriggerReactionEnabled = true,
|
||||
this.discussionTriggerReactionKey,
|
||||
this.discussionTriggerReactionKey = "⏩",
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Custom Mode Options
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
this.customSystemPrompt,
|
||||
this.customTriggerReactionEnabled = true,
|
||||
this.customTriggerReactionKey = "⏩",
|
||||
});
|
||||
|
||||
factory BotOptionsModel.fromJson(json) {
|
||||
return BotOptionsModel(
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// General Bot Options
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
languageLevel: json[ModelKey.languageLevel],
|
||||
safetyModeration: json[ModelKey.safetyModeration] ?? true,
|
||||
mode: json[ModelKey.mode] ?? "discussion",
|
||||
custom: json[ModelKey.custom],
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Discussion Mode Options
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
discussionTopic: json[ModelKey.discussionTopic],
|
||||
discussionKeywords: json[ModelKey.discussionKeywords],
|
||||
discussionTriggerScheduleEnabled:
|
||||
json[ModelKey.discussionTriggerScheduleEnabled],
|
||||
discussionTriggerScheduleHourInterval:
|
||||
json[ModelKey.discussionTriggerScheduleHourInterval],
|
||||
discussionTriggerReactionEnabled:
|
||||
json[ModelKey.discussionTriggerReactionEnabled],
|
||||
discussionTriggerReactionKey: json[ModelKey.discussionTriggerReactionKey],
|
||||
json[ModelKey.discussionTriggerReactionEnabled] ?? true,
|
||||
discussionTriggerReactionKey:
|
||||
json[ModelKey.discussionTriggerReactionKey] ?? "⏩",
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Custom Mode Options
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
customSystemPrompt: json[ModelKey.customSystemPrompt],
|
||||
customTriggerReactionEnabled:
|
||||
json[ModelKey.customTriggerReactionEnabled] ?? true,
|
||||
customTriggerReactionKey: json[ModelKey.customTriggerReactionKey] ?? "⏩",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -61,17 +83,16 @@ class BotOptionsModel {
|
|||
data[ModelKey.languageLevel] = languageLevel;
|
||||
data[ModelKey.safetyModeration] = safetyModeration;
|
||||
data[ModelKey.mode] = mode;
|
||||
data[ModelKey.custom] = custom;
|
||||
data[ModelKey.discussionTopic] = discussionTopic;
|
||||
data[ModelKey.discussionKeywords] = discussionKeywords;
|
||||
data[ModelKey.discussionTriggerScheduleEnabled] =
|
||||
discussionTriggerScheduleEnabled;
|
||||
data[ModelKey.discussionTriggerScheduleHourInterval] =
|
||||
discussionTriggerScheduleHourInterval;
|
||||
data[ModelKey.discussionTriggerReactionEnabled] =
|
||||
discussionTriggerReactionEnabled;
|
||||
discussionTriggerReactionEnabled ?? true;
|
||||
data[ModelKey.discussionTriggerReactionKey] =
|
||||
discussionTriggerReactionKey;
|
||||
discussionTriggerReactionKey ?? "⏩";
|
||||
data[ModelKey.customSystemPrompt] = customSystemPrompt;
|
||||
data[ModelKey.customTriggerReactionEnabled] =
|
||||
customTriggerReactionEnabled ?? true;
|
||||
data[ModelKey.customTriggerReactionKey] = customTriggerReactionKey ?? "⏩";
|
||||
return data;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -92,27 +113,27 @@ class BotOptionsModel {
|
|||
case ModelKey.mode:
|
||||
mode = value;
|
||||
break;
|
||||
case ModelKey.custom:
|
||||
custom = value;
|
||||
break;
|
||||
case ModelKey.discussionTopic:
|
||||
discussionTopic = value;
|
||||
break;
|
||||
case ModelKey.discussionKeywords:
|
||||
discussionKeywords = value;
|
||||
break;
|
||||
case ModelKey.discussionTriggerScheduleEnabled:
|
||||
discussionTriggerScheduleEnabled = value;
|
||||
break;
|
||||
case ModelKey.discussionTriggerScheduleHourInterval:
|
||||
discussionTriggerScheduleHourInterval = value;
|
||||
break;
|
||||
case ModelKey.discussionTriggerReactionEnabled:
|
||||
discussionTriggerReactionEnabled = value;
|
||||
break;
|
||||
case ModelKey.discussionTriggerReactionKey:
|
||||
discussionTriggerReactionKey = value;
|
||||
break;
|
||||
case ModelKey.customSystemPrompt:
|
||||
customSystemPrompt = value;
|
||||
break;
|
||||
case ModelKey.customTriggerReactionEnabled:
|
||||
customTriggerReactionEnabled = value;
|
||||
break;
|
||||
case ModelKey.customTriggerReactionKey:
|
||||
customTriggerReactionKey = value;
|
||||
break;
|
||||
default:
|
||||
throw Exception('Invalid key for bot options - $key');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
|
||||
import '../constants/choreo_constants.dart';
|
||||
import '../enum/construct_type_enum.dart';
|
||||
import 'it_step.dart';
|
||||
import 'lemma.dart';
|
||||
|
||||
/// this class lives within a [PangeaIGCEvent]
|
||||
/// it always has a [RepresentationEvent] parent
|
||||
|
|
@ -111,135 +106,6 @@ class ChoreoRecord {
|
|||
openMatches: [],
|
||||
);
|
||||
|
||||
/// [tokens] is the final list of tokens that were sent
|
||||
/// if no ga or ta,
|
||||
/// make wa use for each and return
|
||||
/// else
|
||||
/// for each saveable vocab in the final message
|
||||
/// if vocab is contained in an accepted replacement, make ga use
|
||||
/// if vocab is contained in ta choice,
|
||||
/// if selected as choice, corIt
|
||||
/// if written as customInput, corIt? (account for score in this)
|
||||
/// for each it step
|
||||
/// for each continuance
|
||||
/// if not within the final message, save ignIT/incIT
|
||||
List<OneConstructUse> toVocabUse(
|
||||
List<PangeaToken> tokens,
|
||||
String chatId,
|
||||
String msgId,
|
||||
DateTime timestamp,
|
||||
) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
final DateTime now = DateTime.now();
|
||||
List<OneConstructUse> lemmasToVocabUses(
|
||||
List<Lemma> lemmas,
|
||||
ConstructUseType type,
|
||||
) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
for (final lemma in lemmas) {
|
||||
if (lemma.saveVocab) {
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
useType: type,
|
||||
chatId: chatId,
|
||||
timeStamp: timestamp,
|
||||
lemma: lemma.text,
|
||||
form: lemma.form,
|
||||
msgId: msgId,
|
||||
constructType: ConstructType.vocab,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
List<OneConstructUse> getVocabUseForToken(PangeaToken token) {
|
||||
for (final step in choreoSteps) {
|
||||
/// if 1) accepted match 2) token is in the replacement and 3) replacement
|
||||
/// is in the overall step text, then token was a ga
|
||||
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted &&
|
||||
(step.acceptedOrIgnoredMatch!.match.choices?.any(
|
||||
(r) =>
|
||||
r.value.contains(token.text.content) &&
|
||||
step.text.contains(r.value),
|
||||
) ??
|
||||
false)) {
|
||||
return lemmasToVocabUses(token.lemmas, ConstructUseType.ga);
|
||||
}
|
||||
if (step.itStep != null) {
|
||||
final bool pickedThroughIT = step.itStep!.chosenContinuance?.text
|
||||
.contains(token.text.content) ??
|
||||
false;
|
||||
if (pickedThroughIT) {
|
||||
return lemmasToVocabUses(token.lemmas, ConstructUseType.corIt);
|
||||
//PTODO - check if added via custom input in IT flow
|
||||
}
|
||||
}
|
||||
}
|
||||
return lemmasToVocabUses(token.lemmas, ConstructUseType.wa);
|
||||
}
|
||||
|
||||
/// for each token, record whether selected in ga, ta, or wa
|
||||
for (final token in tokens) {
|
||||
uses.addAll(getVocabUseForToken(token));
|
||||
}
|
||||
|
||||
for (final itStep in itSteps) {
|
||||
for (final continuance in itStep.continuances) {
|
||||
// this seems to always be false for continuances right now
|
||||
|
||||
if (finalMessage.contains(continuance.text)) {
|
||||
continue;
|
||||
}
|
||||
if (continuance.wasClicked) {
|
||||
//PTODO - account for end of flow score
|
||||
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
|
||||
uses.addAll(
|
||||
lemmasToVocabUses(continuance.lemmas, ConstructUseType.incIt),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (continuance.level != ChoreoConstants.levelThresholdForGreen) {
|
||||
uses.addAll(
|
||||
lemmasToVocabUses(continuance.lemmas, ConstructUseType.ignIt),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uses;
|
||||
}
|
||||
|
||||
List<OneConstructUse> toGrammarConstructUse(
|
||||
String msgId,
|
||||
String chatId,
|
||||
DateTime timestamp,
|
||||
) {
|
||||
final List<OneConstructUse> uses = [];
|
||||
for (final step in choreoSteps) {
|
||||
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) {
|
||||
final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ??
|
||||
step.acceptedOrIgnoredMatch!.match.shortMessage ??
|
||||
step.acceptedOrIgnoredMatch!.match.type.typeName.name;
|
||||
uses.add(
|
||||
OneConstructUse(
|
||||
useType: ConstructUseType.ga,
|
||||
chatId: chatId,
|
||||
timeStamp: timestamp,
|
||||
lemma: name,
|
||||
form: name,
|
||||
msgId: msgId,
|
||||
constructType: ConstructType.grammar,
|
||||
id: "${msgId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return uses;
|
||||
}
|
||||
|
||||
List<ITStep> get itSteps =>
|
||||
choreoSteps.where((e) => e.itStep != null).map((e) => e.itStep!).toList();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -154,32 +155,37 @@ class VocabTotals {
|
|||
void addVocabUseBasedOnUseType(List<OneConstructUse> uses) {
|
||||
for (final use in uses) {
|
||||
switch (use.useType) {
|
||||
case ConstructUseType.ga:
|
||||
case ConstructUseTypeEnum.ga:
|
||||
ga++;
|
||||
break;
|
||||
case ConstructUseType.wa:
|
||||
case ConstructUseTypeEnum.wa:
|
||||
wa++;
|
||||
break;
|
||||
case ConstructUseType.corIt:
|
||||
case ConstructUseTypeEnum.corIt:
|
||||
corIt++;
|
||||
break;
|
||||
case ConstructUseType.incIt:
|
||||
case ConstructUseTypeEnum.incIt:
|
||||
incIt++;
|
||||
break;
|
||||
case ConstructUseType.ignIt:
|
||||
case ConstructUseTypeEnum.ignIt:
|
||||
ignIt++;
|
||||
break;
|
||||
//TODO - these shouldn't be counted as such
|
||||
case ConstructUseType.ignIGC:
|
||||
case ConstructUseTypeEnum.ignIGC:
|
||||
ignIt++;
|
||||
break;
|
||||
case ConstructUseType.corIGC:
|
||||
case ConstructUseTypeEnum.corIGC:
|
||||
corIt++;
|
||||
break;
|
||||
case ConstructUseType.incIGC:
|
||||
case ConstructUseTypeEnum.incIGC:
|
||||
incIt++;
|
||||
break;
|
||||
case ConstructUseType.unk:
|
||||
//TODO if we bring back Headwords then we need to add these
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
break;
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
break;
|
||||
case ConstructUseTypeEnum.unk:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/language_detection_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/span_card_model.dart';
|
||||
|
|
@ -13,12 +14,11 @@ import 'package:matrix/matrix.dart';
|
|||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../constants/model_keys.dart';
|
||||
import 'language_detection_model.dart';
|
||||
|
||||
// import 'package:language_tool/language_tool.dart';
|
||||
|
||||
class IGCTextData {
|
||||
List<LanguageDetection> detections;
|
||||
LanguageDetectionResponse detections;
|
||||
String originalInput;
|
||||
String? fullTextCorrection;
|
||||
List<PangeaToken> tokens;
|
||||
|
|
@ -42,6 +42,18 @@ class IGCTextData {
|
|||
});
|
||||
|
||||
factory IGCTextData.fromJson(Map<String, dynamic> json) {
|
||||
// changing this to allow for use of the LanguageDetectionResponse methods
|
||||
// TODO - change API after we're sure all clients are updated. not urgent.
|
||||
final LanguageDetectionResponse detections =
|
||||
json[_detectionsKey] is Iterable
|
||||
? LanguageDetectionResponse.fromJson({
|
||||
"detections": json[_detectionsKey],
|
||||
"full_text": json["original_input"],
|
||||
})
|
||||
: LanguageDetectionResponse.fromJson(
|
||||
json[_detectionsKey] as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
return IGCTextData(
|
||||
tokens: (json[_tokensKey] as Iterable)
|
||||
.map<PangeaToken>(
|
||||
|
|
@ -59,12 +71,7 @@ class IGCTextData {
|
|||
.toList()
|
||||
.cast<PangeaMatch>()
|
||||
: [],
|
||||
detections: (json[_detectionsKey] as Iterable)
|
||||
.map<LanguageDetection>(
|
||||
(e) => LanguageDetection.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
.cast<LanguageDetection>(),
|
||||
detections: detections,
|
||||
originalInput: json["original_input"],
|
||||
fullTextCorrection: json["full_text_correction"],
|
||||
userL1: json[ModelKey.userL1],
|
||||
|
|
@ -79,7 +86,7 @@ class IGCTextData {
|
|||
static const String _detectionsKey = "detections";
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
_detectionsKey: detections.map((e) => e.toJson()).toList(),
|
||||
_detectionsKey: detections.toJson(),
|
||||
"original_input": originalInput,
|
||||
"full_text_correction": fullTextCorrection,
|
||||
_tokensKey: tokens.map((e) => e.toJson()).toList(),
|
||||
|
|
@ -90,6 +97,18 @@ class IGCTextData {
|
|||
"enable_igc": enableIGC,
|
||||
};
|
||||
|
||||
/// if we haven't run IGC or IT or there are no matches, we use the highest validated detection
|
||||
/// from [LanguageDetectionResponse.highestValidatedDetection]
|
||||
/// if we have run igc/it and there are no matches, we can relax the threshold
|
||||
/// and use the highest confidence detection
|
||||
String get detectedLanguage {
|
||||
if (!(enableIGC && enableIT) || matches.isNotEmpty) {
|
||||
return detections.highestValidatedDetection().langCode;
|
||||
} else {
|
||||
return detections.highestConfidenceDetection.langCode;
|
||||
}
|
||||
}
|
||||
|
||||
// reconstruct fullText based on accepted match
|
||||
//update offsets in existing matches to reflect the change
|
||||
//if existing matches overlap with the accepted one, remove them??
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/language_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/language_constants.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
/// Represents a lemma object
|
||||
class Lemma {
|
||||
/// [text] ex "ir" - text of the lemma of the word
|
||||
final String text;
|
||||
|
||||
/// [form] ex "vamos" - conjugated form of the lemma and as it appeared in some original text
|
||||
final String form;
|
||||
|
||||
/// [saveVocab] true - whether to save the lemma to the user's vocabulary
|
||||
/// vocab that are not saved: emails, urls, numbers, punctuation, etc.
|
||||
final bool saveVocab;
|
||||
|
||||
Lemma({required this.text, required this.saveVocab, required this.form});
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class MultipleChoice {
|
|||
return MultipleChoice(
|
||||
question: json['question'] as String,
|
||||
choices: (json['choices'] as List).map((e) => e as String).toList(),
|
||||
answer: json['answer'] as String,
|
||||
answer: json['answer'] ?? json['correct_answer'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart';
|
|||
|
||||
class ConstructIdentifier {
|
||||
final String lemma;
|
||||
final ConstructType type;
|
||||
final ConstructTypeEnum type;
|
||||
|
||||
ConstructIdentifier({required this.lemma, required this.type});
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ class ConstructIdentifier {
|
|||
try {
|
||||
return ConstructIdentifier(
|
||||
lemma: json['lemma'] as String,
|
||||
type: ConstructType.values.firstWhere(
|
||||
type: ConstructTypeEnum.values.firstWhere(
|
||||
(e) => e.string == json['type'],
|
||||
),
|
||||
);
|
||||
|
|
@ -243,9 +243,11 @@ class PracticeActivityModel {
|
|||
.toList(),
|
||||
langCode: json['lang_code'] as String,
|
||||
msgId: json['msg_id'] as String,
|
||||
activityType: ActivityTypeEnum.values.firstWhere(
|
||||
(e) => e.string == json['activity_type'],
|
||||
),
|
||||
activityType: json['activity_type'] == "multipleChoice"
|
||||
? ActivityTypeEnum.multipleChoice
|
||||
: ActivityTypeEnum.values.firstWhere(
|
||||
(e) => e.string == json['activity_type'],
|
||||
),
|
||||
multipleChoice: json['multiple_choice'] != null
|
||||
? MultipleChoice.fromJson(
|
||||
json['multiple_choice'] as Map<String, dynamic>,
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
@ -54,6 +54,7 @@ class PUserModel {
|
|||
enum MatrixProfile {
|
||||
dateOfBirth,
|
||||
autoPlayMessages,
|
||||
itAutoPlay,
|
||||
activatedFreeTrial,
|
||||
interactiveTranslator,
|
||||
interactiveGrammar,
|
||||
|
|
@ -79,6 +80,8 @@ extension MatrixProfileExtension on MatrixProfile {
|
|||
return ModelKey.userDateOfBirth;
|
||||
case MatrixProfile.autoPlayMessages:
|
||||
return PLocalKey.autoPlayMessages;
|
||||
case MatrixProfile.itAutoPlay:
|
||||
return PLocalKey.itAutoPlay;
|
||||
case MatrixProfile.activatedFreeTrial:
|
||||
return PLocalKey.activatedTrialKey;
|
||||
case MatrixProfile.interactiveTranslator:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ class AnalyticsLanguageButton extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<LanguageModel>(
|
||||
icon: const Icon(Icons.language_outlined),
|
||||
tooltip: L10n.of(context)!.changeAnalyticsLanguage,
|
||||
initialValue: value,
|
||||
onSelected: (LanguageModel? lang) {
|
||||
|
|
@ -33,6 +32,19 @@ class AnalyticsLanguageButton extends StatelessWidget {
|
|||
child: Text(lang.getDisplayName(context) ?? lang.langCode),
|
||||
);
|
||||
}).toList(),
|
||||
child: TextButton.icon(
|
||||
label: Text(
|
||||
value.getDisplayName(context) ?? value.langCode,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.language_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
onPressed: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,21 +20,25 @@ 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;
|
||||
final StudentAnalyticsController? myAnalyticsController;
|
||||
final List<LanguageModel> targetLanguages;
|
||||
|
||||
const BaseAnalyticsPage({
|
||||
BaseAnalyticsPage({
|
||||
super.key,
|
||||
required this.pageTitle,
|
||||
required this.tabs,
|
||||
required this.alwaysSelected,
|
||||
required this.defaultSelected,
|
||||
this.selectedView,
|
||||
required this.selectedView,
|
||||
this.myAnalyticsController,
|
||||
});
|
||||
targetLanguages,
|
||||
}) : targetLanguages = (targetLanguages?.isNotEmpty ?? false)
|
||||
? targetLanguages
|
||||
: MatrixState.pangeaController.pLanguageStore.targetOptions;
|
||||
|
||||
@override
|
||||
State<BaseAnalyticsPage> createState() => BaseAnalyticsController();
|
||||
|
|
@ -46,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;
|
||||
|
||||
|
|
@ -59,6 +64,7 @@ class BaseAnalyticsController extends State<BaseAnalyticsPage> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
currentView = widget.selectedView;
|
||||
if (widget.defaultSelected.type == AnalyticsEntryType.student) {
|
||||
runFirstRefresh();
|
||||
}
|
||||
|
|
@ -159,7 +165,13 @@ class BaseAnalyticsController extends State<BaseAnalyticsPage> {
|
|||
}
|
||||
|
||||
Future<void> toggleSpaceLang(LanguageModel lang) async {
|
||||
await pangeaController.analytics.setCurrentAnalyticsSpaceLang(lang);
|
||||
await pangeaController.analytics.setCurrentAnalyticsLang(lang);
|
||||
await setChartData();
|
||||
refreshStream.add(false);
|
||||
}
|
||||
|
||||
Future<void> toggleView(BarChartViewSelection view) async {
|
||||
currentView = view;
|
||||
await setChartData();
|
||||
refreshStream.add(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,223 +87,156 @@ class BaseAnalyticsView extends StatelessWidget {
|
|||
),
|
||||
body: MaxWidthBody(
|
||||
withScrolling: false,
|
||||
child: controller.widget.selectedView != null
|
||||
? Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (controller.widget.defaultSelected.type ==
|
||||
AnalyticsEntryType.student)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: controller.onRefresh,
|
||||
tooltip: L10n.of(context)!.refresh,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
TimeSpanMenuButton(
|
||||
value: controller.currentTimeSpan,
|
||||
onChange: (TimeSpan value) =>
|
||||
controller.toggleTimeSpan(context, value),
|
||||
),
|
||||
if (controller.widget.defaultSelected.type ==
|
||||
AnalyticsEntryType.space)
|
||||
AnalyticsLanguageButton(
|
||||
value: controller.pangeaController.analytics
|
||||
.currentAnalyticsSpaceLang,
|
||||
onChange: (lang) => controller.toggleSpaceLang(lang),
|
||||
languages: controller
|
||||
.pangeaController.pLanguageStore.targetOptions,
|
||||
),
|
||||
],
|
||||
),
|
||||
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 = [];
|
||||
|
|
@ -355,15 +355,17 @@ class ConstructMessagesDialog extends StatelessWidget {
|
|||
|
||||
final msgEventMatches = controller.getMessageEventMatches();
|
||||
|
||||
final noData = controller.constructs![controller.lemmaIndex].uses.length >
|
||||
controller._msgEvents.length;
|
||||
|
||||
return AlertDialog(
|
||||
title: Center(child: Text(controller.widget.controller.currentLemma!)),
|
||||
content: SizedBox(
|
||||
height: 350,
|
||||
width: 500,
|
||||
height: noData ? 90 : 250,
|
||||
width: noData ? 200 : 400,
|
||||
child: Column(
|
||||
children: [
|
||||
if (controller.constructs![controller.lemmaIndex].uses.length >
|
||||
controller._msgEvents.length)
|
||||
if (noData)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
|
|
@ -398,8 +400,8 @@ class ConstructMessagesDialog extends StatelessWidget {
|
|||
child: Text(
|
||||
L10n.of(context)!.close.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color:
|
||||
Theme.of(context).textTheme.bodyMedium?.color?.withAlpha(150),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'dart:developer';
|
|||
import 'package:fluffychat/pangea/constants/pangea_room_types.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:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
|
||||
import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart';
|
||||
|
|
@ -17,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();
|
||||
|
|
@ -33,6 +34,18 @@ class SpaceAnalyticsV2Controller extends State<SpaceAnalyticsPage> {
|
|||
List<User> students = [];
|
||||
String? get spaceId => GoRouterState.of(context).pathParameters['spaceid'];
|
||||
Room? _spaceRoom;
|
||||
List<LanguageModel> targetLanguages = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Future.delayed(Duration.zero, () async {
|
||||
if (spaceRoom == null || (!(spaceRoom?.isSpace ?? false))) {
|
||||
context.go('/rooms');
|
||||
}
|
||||
getChatAndStudents();
|
||||
});
|
||||
}
|
||||
|
||||
Room? get spaceRoom {
|
||||
if (_spaceRoom == null || _spaceRoom!.id != spaceId) {
|
||||
|
|
@ -44,23 +57,11 @@ class SpaceAnalyticsV2Controller extends State<SpaceAnalyticsPage> {
|
|||
context.go('/rooms/analytics');
|
||||
return null;
|
||||
}
|
||||
getChatAndStudents();
|
||||
getChatAndStudents().then((_) => setTargetLanguages());
|
||||
}
|
||||
return _spaceRoom;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
debugPrint("init space analytics");
|
||||
Future.delayed(Duration.zero, () async {
|
||||
if (spaceRoom == null || (!(spaceRoom?.isSpace ?? false))) {
|
||||
context.go('/rooms');
|
||||
}
|
||||
getChatAndStudents();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> getChatAndStudents() async {
|
||||
try {
|
||||
await spaceRoom?.postLoad();
|
||||
|
|
@ -97,12 +98,12 @@ class SpaceAnalyticsV2Controller extends State<SpaceAnalyticsPage> {
|
|||
}
|
||||
}
|
||||
|
||||
// @override
|
||||
// void dispose() {
|
||||
// super.dispose();
|
||||
// refreshTimer?.cancel();
|
||||
// stateSub?.cancel();
|
||||
// }
|
||||
Future<void> setTargetLanguages() async {
|
||||
// get a list of language models, sorted by the
|
||||
// number of students who are learning that language
|
||||
targetLanguages = await spaceRoom?.targetLanguages() ?? [];
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ class SpaceAnalyticsView extends StatelessWidget {
|
|||
AnalyticsEntryType.space,
|
||||
controller.spaceRoom?.name ?? "",
|
||||
),
|
||||
targetLanguages: controller.targetLanguages,
|
||||
)
|
||||
: const SizedBox();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
|
||||
import 'package:fluffychat/pangea/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/space_list/space_list_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -22,26 +23,58 @@ class AnalyticsSpaceList extends StatefulWidget {
|
|||
class AnalyticsSpaceListController extends State<AnalyticsSpaceList> {
|
||||
PangeaController pangeaController = MatrixState.pangeaController;
|
||||
List<Room> spaces = [];
|
||||
StreamSubscription? stateSub;
|
||||
List<LanguageModel> targetLanguages = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
Matrix.of(context).client.spacesImTeaching.then((spaceList) {
|
||||
spaceList = spaceList
|
||||
.where(
|
||||
(space) => !spaceList.any(
|
||||
(parentSpace) => parentSpace.spaceChildren
|
||||
.any((child) => child.roomId == space.id),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
spaces = spaceList;
|
||||
setState(() {});
|
||||
});
|
||||
setSpaceList().then((_) => setTargetLanguages());
|
||||
|
||||
// reload dropdowns when their values change in analytics page
|
||||
stateSub = pangeaController.analytics.stateStream.listen(
|
||||
(_) => setState(() {}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
stateSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
StreamController refreshStream = StreamController.broadcast();
|
||||
|
||||
Future<void> setSpaceList() async {
|
||||
final spaceList = await Matrix.of(context).client.spacesImTeaching;
|
||||
spaces = spaceList
|
||||
.where(
|
||||
(space) => !spaceList.any(
|
||||
(parentSpace) => parentSpace.spaceChildren
|
||||
.any((child) => child.roomId == space.id),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> setTargetLanguages() async {
|
||||
if (spaces.isEmpty) return;
|
||||
final Map<LanguageModel, int> langCounts = {};
|
||||
for (final Room space in spaces) {
|
||||
final List<LanguageModel> targetLangs = await space.targetLanguages();
|
||||
for (final LanguageModel lang in targetLangs) {
|
||||
langCounts[lang] ??= 0;
|
||||
langCounts[lang] = langCounts[lang]! + 1;
|
||||
}
|
||||
}
|
||||
targetLanguages = langCounts.entries.map((entry) => entry.key).toList()
|
||||
..sort(
|
||||
(a, b) => langCounts[b]!.compareTo(langCounts[a]!),
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void toggleTimeSpan(BuildContext context, TimeSpan timeSpan) {
|
||||
pangeaController.analytics.setCurrentAnalyticsTimeSpan(timeSpan);
|
||||
refreshStream.add(false);
|
||||
|
|
@ -49,7 +82,7 @@ class AnalyticsSpaceListController extends State<AnalyticsSpaceList> {
|
|||
}
|
||||
|
||||
Future<void> toggleSpaceLang(LanguageModel lang) async {
|
||||
await pangeaController.analytics.setCurrentAnalyticsSpaceLang(lang);
|
||||
await pangeaController.analytics.setCurrentAnalyticsLang(lang);
|
||||
refreshStream.add(false);
|
||||
setState(() {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
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/time_span_menu_button.dart';
|
||||
|
|
@ -5,7 +6,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../enum/time_span.dart';
|
||||
import '../base_analytics.dart';
|
||||
import 'space_list.dart';
|
||||
|
||||
|
|
@ -32,25 +32,29 @@ class AnalyticsSpaceListView extends StatelessWidget {
|
|||
icon: const Icon(Icons.close_outlined),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
actions: [
|
||||
TimeSpanMenuButton(
|
||||
value:
|
||||
controller.pangeaController.analytics.currentAnalyticsTimeSpan,
|
||||
onChange: (TimeSpan value) => controller.toggleTimeSpan(
|
||||
context,
|
||||
value,
|
||||
),
|
||||
),
|
||||
AnalyticsLanguageButton(
|
||||
value:
|
||||
controller.pangeaController.analytics.currentAnalyticsSpaceLang,
|
||||
onChange: (lang) => controller.toggleSpaceLang(lang),
|
||||
languages: controller.pangeaController.pLanguageStore.targetOptions,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
TimeSpanMenuButton(
|
||||
value: controller
|
||||
.pangeaController.analytics.currentAnalyticsTimeSpan,
|
||||
onChange: (TimeSpan value) => controller.toggleTimeSpan(
|
||||
context,
|
||||
value,
|
||||
),
|
||||
),
|
||||
AnalyticsLanguageButton(
|
||||
value:
|
||||
controller.pangeaController.analytics.currentAnalyticsLang,
|
||||
onChange: (lang) => controller.toggleSpaceLang(lang),
|
||||
languages:
|
||||
controller.pangeaController.pLanguageStore.targetOptions,
|
||||
),
|
||||
],
|
||||
),
|
||||
Flexible(
|
||||
child: ListView.builder(
|
||||
itemCount: controller.spaces.length,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
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';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/language_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/common/list_placeholder.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -14,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();
|
||||
|
|
@ -24,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 {
|
||||
|
|
@ -75,6 +65,24 @@ class StudentAnalyticsController extends State<StudentAnalyticsPage> {
|
|||
return id;
|
||||
}
|
||||
|
||||
List<LanguageModel> get targetLanguages {
|
||||
final LanguageModel? l2 =
|
||||
_pangeaController.languageController.activeL2Model();
|
||||
final List<LanguageModel> analyticsRoomLangs =
|
||||
_pangeaController.matrixState.client.allMyAnalyticsRooms
|
||||
.map((analyticsRoom) => analyticsRoom.madeForLang)
|
||||
.where((langCode) => langCode != null)
|
||||
.map((langCode) => PangeaLanguage.byLangCode(langCode!))
|
||||
.where(
|
||||
(langModel) => langModel.langCode != LanguageKeys.unknownLanguage,
|
||||
)
|
||||
.toList();
|
||||
if (l2 != null) {
|
||||
analyticsRoomLangs.add(l2);
|
||||
}
|
||||
return analyticsRoomLangs.toSet().toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PLoadingStatusV2(
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ class StudentAnalyticsView extends StatelessWidget {
|
|||
AnalyticsEntryType.student,
|
||||
L10n.of(context)!.allChatsAndClasses,
|
||||
),
|
||||
targetLanguages: controller.targetLanguages,
|
||||
)
|
||||
: const SizedBox();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ class TimeSpanMenuButton extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<TimeSpan>(
|
||||
icon: const Icon(Icons.calendar_month_outlined),
|
||||
tooltip: L10n.of(context)!.changeDateRange,
|
||||
initialValue: value,
|
||||
onSelected: (TimeSpan? timeSpan) {
|
||||
|
|
@ -32,6 +31,19 @@ class TimeSpanMenuButton extends StatelessWidget {
|
|||
child: Text(timeSpan.string(context)),
|
||||
);
|
||||
}).toList(),
|
||||
child: TextButton.icon(
|
||||
label: Text(
|
||||
value.string(context),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.calendar_month_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
onPressed: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class ClassDescriptionButton extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconColor = Theme.of(context).textTheme.bodyLarge!.color;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
|
|
@ -26,14 +27,27 @@ class ClassDescriptionButton extends StatelessWidget {
|
|||
foregroundColor: iconColor,
|
||||
child: const Icon(Icons.topic_outlined),
|
||||
),
|
||||
subtitle: Text(
|
||||
room.topic.isEmpty
|
||||
? (room.isRoomAdmin
|
||||
? (room.isSpace
|
||||
? L10n.of(context)!.classDescriptionDesc
|
||||
: L10n.of(context)!.chatTopicDesc)
|
||||
: L10n.of(context)!.topicNotSet)
|
||||
: room.topic,
|
||||
subtitle: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 190,
|
||||
),
|
||||
child: Scrollbar(
|
||||
controller: scrollController,
|
||||
interactive: true,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
primary: false,
|
||||
child: Text(
|
||||
room.topic.isEmpty
|
||||
? (room.isRoomAdmin
|
||||
? (room.isSpace
|
||||
? L10n.of(context)!.classDescriptionDesc
|
||||
: L10n.of(context)!.chatTopicDesc)
|
||||
: L10n.of(context)!.topicNotSet)
|
||||
: room.topic,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
room.isSpace
|
||||
|
|
|
|||
|
|
@ -61,6 +61,16 @@ class SettingsLearningView extends StatelessWidget {
|
|||
pStoreKey: setting.toString(),
|
||||
local: false,
|
||||
),
|
||||
PSettingsSwitchListTile.adaptive(
|
||||
defaultValue: controller.pangeaController.pStoreService.read(
|
||||
PLocalKey.itAutoPlay,
|
||||
) ??
|
||||
false,
|
||||
title: L10n.of(context)!.interactiveTranslatorAutoPlaySliderHeader,
|
||||
subtitle: L10n.of(context)!.interactiveTranslatorAutoPlayDesc,
|
||||
pStoreKey: PLocalKey.itAutoPlay,
|
||||
local: false,
|
||||
),
|
||||
PSettingsSwitchListTile.adaptive(
|
||||
defaultValue: controller.pangeaController.pStoreService.read(
|
||||
PLocalKey.autoPlayMessages,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_detection_controller.dart';
|
||||
import 'package:fluffychat/pangea/models/language_detection_model.dart';
|
||||
import 'package:fluffychat/pangea/models/lemma.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
|
||||
|
|
@ -39,7 +40,10 @@ class IgcRepo {
|
|||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
final IGCTextData igcTextData = IGCTextData(
|
||||
detections: [LanguageDetection(langCode: "en", confidence: 0.99)],
|
||||
detections: LanguageDetectionResponse(
|
||||
detections: [LanguageDetection(langCode: "en", confidence: 0.99)],
|
||||
fullText: "This be a sample text",
|
||||
),
|
||||
tokens: [
|
||||
PangeaToken(
|
||||
text: PangeaTokenText(content: "This", offset: 0, length: 4),
|
||||
|
|
@ -89,7 +93,6 @@ class IGCRequestBody {
|
|||
String fullText;
|
||||
String userL1;
|
||||
String userL2;
|
||||
bool tokensOnly;
|
||||
bool enableIT;
|
||||
bool enableIGC;
|
||||
|
||||
|
|
@ -99,7 +102,6 @@ class IGCRequestBody {
|
|||
required this.userL2,
|
||||
required this.enableIGC,
|
||||
required this.enableIT,
|
||||
this.tokensOnly = false,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
|
|
@ -108,6 +110,5 @@ class IGCRequestBody {
|
|||
ModelKey.userL2: userL2,
|
||||
"enable_it": enableIT,
|
||||
"enable_igc": enableIGC,
|
||||
"tokens_only": tokensOnly,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
|
|||
|
||||
class PangeaAnyState {
|
||||
final Map<String, LayerLinkAndKey> _layerLinkAndKeys = {};
|
||||
OverlayEntry? overlay;
|
||||
List<OverlayEntry> entries = [];
|
||||
|
||||
dispose() {
|
||||
closeOverlay();
|
||||
|
|
@ -32,26 +32,32 @@ class PangeaAnyState {
|
|||
_layerLinkAndKeys.remove(transformTargetId);
|
||||
}
|
||||
|
||||
void openOverlay(OverlayEntry entry, BuildContext context) {
|
||||
closeOverlay();
|
||||
overlay = entry;
|
||||
Overlay.of(context).insert(overlay!);
|
||||
void openOverlay(
|
||||
OverlayEntry entry,
|
||||
BuildContext context, {
|
||||
bool closePrevOverlay = true,
|
||||
}) {
|
||||
if (closePrevOverlay) {
|
||||
closeOverlay();
|
||||
}
|
||||
entries.add(entry);
|
||||
Overlay.of(context).insert(entry);
|
||||
}
|
||||
|
||||
void closeOverlay() {
|
||||
if (overlay != null) {
|
||||
if (entries.isNotEmpty) {
|
||||
try {
|
||||
overlay?.remove();
|
||||
entries.last.remove();
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
s: s,
|
||||
data: {
|
||||
"overlay": overlay,
|
||||
"overlay": entries.last,
|
||||
},
|
||||
);
|
||||
}
|
||||
overlay = null;
|
||||
entries.removeLast();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,12 +48,19 @@ 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, [
|
||||
bool showToggle = true,
|
||||
]) async {
|
||||
if (_instructionsShown[key] ?? false) {
|
||||
return;
|
||||
}
|
||||
_instructionsShown[key] = true;
|
||||
|
||||
if (wereInstructionsTurnedOff(key)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -51,9 +71,6 @@ class InstructionsController {
|
|||
);
|
||||
return;
|
||||
}
|
||||
if (_instructionsShown[key] ?? false) {
|
||||
return;
|
||||
}
|
||||
|
||||
final bool userLangsSet =
|
||||
await _pangeaController.userController.areUserLanguagesSet;
|
||||
|
|
@ -61,8 +78,6 @@ class InstructionsController {
|
|||
return;
|
||||
}
|
||||
|
||||
_instructionsShown[key] = true;
|
||||
|
||||
final botStyle = BotStyle.text(context);
|
||||
Future.delayed(
|
||||
const Duration(seconds: 1),
|
||||
|
|
@ -74,7 +89,7 @@ class InstructionsController {
|
|||
children: [
|
||||
CardHeader(
|
||||
text: key.title(context),
|
||||
botExpression: BotExpression.surprised,
|
||||
botExpression: BotExpression.idle,
|
||||
onClose: () => {_instructionsClosed[key] = true},
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
|
|
@ -94,48 +109,39 @@ class InstructionsController {
|
|||
),
|
||||
cardSize: const Size(300.0, 300.0),
|
||||
transformTargetId: transformTargetKey,
|
||||
closePrevOverlay: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// User can toggle on to prevent Instruction Card
|
||||
/// from appearing in future sessions
|
||||
class InstructionsToggle extends StatefulWidget {
|
||||
const InstructionsToggle({
|
||||
super.key,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/span_data_type.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../constants/match_rule_ids.dart';
|
||||
import '../models/pangea_match_model.dart';
|
||||
|
||||
|
|
@ -96,7 +96,11 @@ class MatchCopy {
|
|||
switch (afterColon) {
|
||||
case MatchRuleIds.interactiveTranslation:
|
||||
title = l10n.needsItShortMessage;
|
||||
description = l10n.needsItMessage;
|
||||
description = l10n.needsItMessage(
|
||||
MatrixState
|
||||
.pangeaController.languageController.userL2?.displayName ??
|
||||
"target language",
|
||||
);
|
||||
break;
|
||||
case MatchRuleIds.tokenNeedsTranslation:
|
||||
title = l10n.tokenTranslationTitle;
|
||||
|
|
|
|||
|
|
@ -25,9 +25,12 @@ class OverlayUtil {
|
|||
Color? backgroundColor,
|
||||
Alignment? targetAnchor,
|
||||
Alignment? followerAnchor,
|
||||
bool closePrevOverlay = true,
|
||||
}) {
|
||||
try {
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
if (closePrevOverlay) {
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
}
|
||||
final LayerLinkAndKey layerLinkAndKey =
|
||||
MatrixState.pAnyState.layerLinkAndKey(transformTargetId);
|
||||
|
||||
|
|
@ -58,7 +61,8 @@ class OverlayUtil {
|
|||
),
|
||||
);
|
||||
|
||||
MatrixState.pAnyState.openOverlay(entry, context);
|
||||
MatrixState.pAnyState
|
||||
.openOverlay(entry, context, closePrevOverlay: closePrevOverlay);
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: err, s: stack);
|
||||
|
|
@ -72,6 +76,7 @@ class OverlayUtil {
|
|||
required String transformTargetId,
|
||||
backDropToDismiss = true,
|
||||
Color? borderColor,
|
||||
bool closePrevOverlay = true,
|
||||
}) {
|
||||
try {
|
||||
final LayerLinkAndKey layerLinkAndKey =
|
||||
|
|
@ -105,6 +110,7 @@ class OverlayUtil {
|
|||
offset: cardOffset,
|
||||
backDropToDismiss: backDropToDismiss,
|
||||
borderColor: borderColor,
|
||||
closePrevOverlay: closePrevOverlay,
|
||||
);
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -180,7 +186,7 @@ class OverlayUtil {
|
|||
return Offset(dx, dy);
|
||||
}
|
||||
|
||||
static bool get isOverlayOpen => MatrixState.pAnyState.overlay != null;
|
||||
static bool get isOverlayOpen => MatrixState.pAnyState.entries.isNotEmpty;
|
||||
}
|
||||
|
||||
class TransparentBackdrop extends StatelessWidget {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue