Merge branch 'main' into 688-difficult-to-click-on-a-an-audio-message-for-feedback
This commit is contained in:
commit
25b62b50b5
93 changed files with 3587 additions and 1971 deletions
16
.github/workflows/main_deploy.yaml
vendored
16
.github/workflows/main_deploy.yaml
vendored
|
|
@ -3,22 +3,22 @@ name: Main Deploy Workflow
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- development
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
WEB_APP_ENV: ${{ vars.WEB_APP_ENV }}
|
||||
|
||||
jobs:
|
||||
switch-branch:
|
||||
runs-on: ubuntu-latest
|
||||
# switch-branch:
|
||||
# runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout main branch
|
||||
uses: actions/checkout@v3
|
||||
# steps:
|
||||
# - name: Checkout main branch
|
||||
# uses: actions/checkout@v3
|
||||
|
||||
- name: Checkout different branch
|
||||
run: git checkout development
|
||||
# - name: Checkout different branch
|
||||
# run: git checkout development
|
||||
|
||||
build_web:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
|
|
@ -3135,7 +3135,7 @@
|
|||
"prettyGood": "Pretty good! Here's what I would have said.",
|
||||
"letMeThink": "Hmm, let's see how you did!",
|
||||
"clickMessageTitle": "Need help?",
|
||||
"clickMessageBody": "Click a message for language help! Click and hold to react 😀.",
|
||||
"clickMessageBody": "Click a message for language tools like translation, play back and more!",
|
||||
"understandingMessagesTitle": "Definitions and translations!",
|
||||
"understandingMessagesBody": "Click underlined words for definitions. Translate with message options (upper right).",
|
||||
"allDone": "All done!",
|
||||
|
|
@ -3149,7 +3149,7 @@
|
|||
"generateVocabulary": "Generate vocabulary from title and description",
|
||||
"generatePrompts": "Generate prompts",
|
||||
"subscribe": "Subscribe",
|
||||
"getAccess": "Unlock learning tools",
|
||||
"getAccess": "Subscribe now!",
|
||||
"subscriptionDesc": "Messaging is free! Subscribe to unlock interactive translation, grammar checking and learning analytics.",
|
||||
"subscriptionManagement": "Subscription Management",
|
||||
"currentSubscription": "Current Subscription",
|
||||
|
|
@ -3788,7 +3788,7 @@
|
|||
}
|
||||
},
|
||||
"freeTrialDesc": "New users recieve a one week free trial of Pangea Chat",
|
||||
"activateTrial": "Activate Free Trial",
|
||||
"activateTrial": "Free 7-Day Trial",
|
||||
"inNoSpaces": "You are not a member of any spaces",
|
||||
"successfullySubscribed": "You have successfully subscribed!",
|
||||
"clickToManageSubscription": "Click here to manage your subscription.",
|
||||
|
|
@ -3968,11 +3968,11 @@
|
|||
"seeOptions": "See options",
|
||||
"continuedWithoutSubscription": "Continue without subscribing",
|
||||
"trialPeriodExpired": "Your trial period has expired",
|
||||
"selectToDefine": "Highlight a word or phrase to see its definition!",
|
||||
"selectToDefine": "Click any word to see its definition!",
|
||||
"translations": "translations",
|
||||
"messageAudio": "message audio",
|
||||
"definitions": "definitions",
|
||||
"subscribedToUnlockTools": "Subscribe to unlock language tools, including",
|
||||
"subscribedToUnlockTools": "Subscribe to unlock interactive translation and grammar checking, audio playback, personalized practice activities, and learning analytics!",
|
||||
"more": "More",
|
||||
"translationTooltip": "Translate",
|
||||
"audioTooltip": "Play Audio",
|
||||
|
|
@ -4014,6 +4014,7 @@
|
|||
"conversationBotModeSelectOption_custom": "Custom",
|
||||
"conversationBotModeSelectOption_conversation": "Conversation",
|
||||
"conversationBotModeSelectOption_textAdventure": "Text Adventure",
|
||||
"conversationBotModeSelectOption_storyGame": "Story Game",
|
||||
"conversationBotDiscussionZone_title": "Discussion Settings",
|
||||
"conversationBotDiscussionZone_discussionTopicLabel": "Discussion Topic",
|
||||
"conversationBotDiscussionZone_discussionTopicPlaceholder": "Set Discussion Topic",
|
||||
|
|
@ -4120,7 +4121,7 @@
|
|||
"suggestToSpaceDesc": "Suggested sub spaces will appear in their main space's chat list",
|
||||
"practice": "Practice",
|
||||
"noLanguagesSet": "No languages set",
|
||||
"noActivitiesFound": "No practice activities found for this message",
|
||||
"noActivitiesFound": "That's enough on this for now! Come back later for more.",
|
||||
"hintTitle": "Hint:",
|
||||
"speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores",
|
||||
"previous": "Previous",
|
||||
|
|
@ -4161,7 +4162,7 @@
|
|||
"placeholders": {}
|
||||
},
|
||||
"changeAnalyticsView": "Change Analytics View",
|
||||
"l1TranslationBody": "Oops! It looks like this message wasn't sent in your target language. Messages not sent in your target language will not be translated.",
|
||||
"l1TranslationBody": "Messages in your base language will not be translated.",
|
||||
"continueText": "Continue",
|
||||
"deleteSubscriptionWarningTitle": "You have an active subscription",
|
||||
"deleteSubscriptionWarningBody": "Deleting your account will not automatically cancel your subscription.",
|
||||
|
|
@ -4228,5 +4229,9 @@
|
|||
"grammar": "Grammar",
|
||||
"contactHasBeenInvitedToTheChat": "Contact has been invited to the chat",
|
||||
"inviteChat": "📨 Invite chat",
|
||||
"chatName": "Chat name"
|
||||
"chatName": "Chat name",
|
||||
"reportContentIssueTitle": "Report content issue",
|
||||
"feedback": "Optional feedback",
|
||||
"reportContentIssueDescription": "Uh oh! AI can faciliate personalized learning experiences but... also hallucinates. Please provide any feedback you have and we'll try again.",
|
||||
"clickTheWordAgainToDeselect": "Click the selected word to deselect it."
|
||||
}
|
||||
|
|
@ -4329,7 +4329,6 @@
|
|||
"pleaseTryAgainLaterOrChooseDifferentServer": "",
|
||||
"@pleaseTryAgainLaterOrChooseDifferentServer": {},
|
||||
"createGroup": "",
|
||||
"@createGroup": {},
|
||||
"@noBackupWarning": {},
|
||||
"kickUserDescription": "",
|
||||
"@kickUserDescription": {},
|
||||
|
|
@ -4490,9 +4489,7 @@
|
|||
"translations": "traducciónes",
|
||||
"messageAudio": "mensaje de audio",
|
||||
"definitions": "definiciones",
|
||||
"subscribedToUnlockTools": "Suscríbase para desbloquear herramientas lingüísticas, como",
|
||||
"clickMessageTitle": "¿Necesitas ayuda?",
|
||||
"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",
|
||||
|
|
@ -4505,7 +4502,7 @@
|
|||
"age": {}
|
||||
}
|
||||
},
|
||||
"selectToDefine": "¡Resalta una palabra o frase para ver su definición!",
|
||||
"selectToDefine": "Clic una palabra para definirla",
|
||||
"kickBotWarning": "Patear Pangea Bot eliminará el bot de conversación de este chat.",
|
||||
"activateTrial": "Activar prueba gratuita",
|
||||
"refresh": "Actualizar",
|
||||
|
|
@ -4513,6 +4510,7 @@
|
|||
"autoPlayTitle": "Reproducción automática de mensajes",
|
||||
"autoPlayDesc": "Cuando está activado, el audio de texto a voz de los mensajes se reproducirá automáticamente cuando se seleccione.",
|
||||
"presenceStyle": "Presencia:",
|
||||
"noActivitiesFound": "¡Ya has practicado por ahora! Vuelve más tarde para ver más.",
|
||||
"presencesToggle": "Mostrar mensajes de estado de otros usuarios",
|
||||
"writeAMessageFlag": "Escribe un mensaje en {l1flag} o {l2flag}",
|
||||
"@writeAMessageFlag": {
|
||||
|
|
@ -4685,7 +4683,6 @@
|
|||
"fetchingVersion": "Obteniendo versión...",
|
||||
"versionFetchError": "Error al obtener la versión",
|
||||
"connectedToStaging": "Conectado al entorno de pruebas",
|
||||
"connectedToStaging": "Conectado al entorno de pruebas",
|
||||
"versionText": "Versión: {version}+{buildNumber}",
|
||||
"@versionText": {
|
||||
"description": "Texto que muestra la versión y el número de compilación de la aplicación.",
|
||||
|
|
@ -4709,8 +4706,6 @@
|
|||
},
|
||||
"emojis": "Emojis",
|
||||
"@emojis": {},
|
||||
"createGroup": "Crear grupo",
|
||||
"@createGroup": {},
|
||||
"hydrateTorLong": "¿Exportó su sesión la última vez que estuvo en TOR? Impórtela rápidamente y continúe chateando.",
|
||||
"@hydrateTorLong": {},
|
||||
"custom": "Personalizado",
|
||||
|
|
@ -4736,5 +4731,5 @@
|
|||
}
|
||||
},
|
||||
"commandHint_googly": "Enviar unos ojos saltones",
|
||||
"@commandHint_googly": {}
|
||||
"reportContentIssue": "Problema de contenido"
|
||||
}
|
||||
16
env.ocal_choreo
Normal file
16
env.ocal_choreo
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
BASE_API='https://api.staging.pangea.chat/api/v1'
|
||||
CHOREO_API = "http://localhost:8000/choreo"
|
||||
FRONTEND_URL='https://app.pangea.chat'
|
||||
|
||||
SYNAPSE_URL = 'matrix.staging.pangea.chat'
|
||||
CHOREO_API_KEY = 'e6fa9fa97031ba0c852efe78457922f278a2fbc109752fe18e465337699e9873'
|
||||
|
||||
RC_PROJECT = 'a499dc21'
|
||||
RC_KEY = 'sk_eVGBdPyInaOfJrKlPBgFVnRynqKJB'
|
||||
|
||||
RC_GOOGLE_KEY = 'goog_paQMrzFKGzuWZvcMTPkkvIsifJe'
|
||||
RC_IOS_KEY = 'appl_DUPqnxuLjkBLzhBPTWeDjqNENuv'
|
||||
RC_STRIPE_KEY = 'strp_YWZxWUeEfvagiefDNoofinaRCOl'
|
||||
RC_OFFERING_NAME = 'test'
|
||||
|
||||
STRIPE_MANAGEMENT_LINK = 'https://billing.stripe.com/p/login/test_9AQaI8d3O9lmaXe5kk'
|
||||
|
|
@ -15,10 +15,12 @@ import 'package:fluffychat/pages/chat/event_info_dialog.dart';
|
|||
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/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.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/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
|
|
@ -27,7 +29,6 @@ import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
|||
import 'package:fluffychat/pangea/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/utils/report_message.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/pangea_text_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/user_settings/p_language_dialog.dart';
|
||||
import 'package:fluffychat/utils/error_reporter.dart';
|
||||
|
|
@ -551,6 +552,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
//#Pangea
|
||||
choreographer.stateListener.close();
|
||||
choreographer.dispose();
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
//Pangea#
|
||||
super.dispose();
|
||||
}
|
||||
|
|
@ -652,16 +654,26 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
// There's a listen in my_analytics_controller that decides when to auto-update
|
||||
// analytics based on when / how many messages the logged in user send. This
|
||||
// stream sends the data for newly sent messages.
|
||||
final metadata = ConstructUseMetaData(
|
||||
roomId: roomId,
|
||||
timeStamp: DateTime.now(),
|
||||
eventId: msgEventId,
|
||||
);
|
||||
|
||||
if (msgEventId != null) {
|
||||
pangeaController.myAnalytics.setState(
|
||||
data: {
|
||||
'eventID': msgEventId,
|
||||
'eventType': EventTypes.Message,
|
||||
'roomID': room.id,
|
||||
'originalSent': originalSent,
|
||||
'tokensSent': tokensSent,
|
||||
'choreo': choreo,
|
||||
},
|
||||
AnalyticsStream(
|
||||
eventId: msgEventId,
|
||||
roomId: room.id,
|
||||
constructs: [
|
||||
...(choreo!.grammarConstructUses(metadata: metadata)),
|
||||
...(originalSent!.vocabUses(
|
||||
choreo: choreo,
|
||||
tokens: tokensSent!.tokens,
|
||||
metadata: metadata,
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1303,8 +1315,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
/// text and selection stored for the text in that overlay
|
||||
void closeSelectionOverlay() {
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
textSelection.clearMessageText();
|
||||
textSelection.onSelection(null);
|
||||
// selectedTokenIndicies.clear();
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
|
|
@ -1610,8 +1621,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
});
|
||||
|
||||
// #Pangea
|
||||
final textSelection = MessageTextSelection();
|
||||
|
||||
void showToolbar(
|
||||
PangeaMessageEvent pangeaMessageEvent, {
|
||||
MessageMode? mode,
|
||||
|
|
@ -1643,10 +1652,9 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
Widget? overlayEntry;
|
||||
try {
|
||||
overlayEntry = MessageSelectionOverlay(
|
||||
controller: this,
|
||||
chatController: this,
|
||||
event: pangeaMessageEvent.event,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
textSelection: textSelection,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
);
|
||||
|
|
@ -1671,7 +1679,39 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
onSelectMessage(pangeaMessageEvent.event);
|
||||
HapticFeedback.mediumImpact();
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
// final List<int> selectedTokenIndicies = [];
|
||||
// void onClickOverlayMessageToken(
|
||||
// PangeaMessageEvent pangeaMessageEvent,
|
||||
// int tokenIndex,
|
||||
// ) {
|
||||
// if (pangeaMessageEvent.originalSent?.tokens == null ||
|
||||
// tokenIndex < 0 ||
|
||||
// tokenIndex >= pangeaMessageEvent.originalSent!.tokens!.length) {
|
||||
// selectedTokenIndicies.clear();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // if there's stuff that's already selected, then we already ahve a sentence deselect
|
||||
// if (selectedTokenIndicies.isNotEmpty) {
|
||||
// final bool listContainedIndex =
|
||||
// selectedTokenIndicies.contains(tokenIndex);
|
||||
|
||||
// selectedTokenIndicies.clear();
|
||||
// if (!listContainedIndex) {
|
||||
// selectedTokenIndicies.add(tokenIndex);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // TODO
|
||||
// // if this is already selected, see if there's sentnence and selelct that
|
||||
|
||||
// // if nothing is select, select one token
|
||||
// else {
|
||||
// selectedTokenIndicies.add(tokenIndex);
|
||||
// }
|
||||
// }
|
||||
// // Pangea#
|
||||
|
||||
late final ValueNotifier<bool> displayChatDetailsColumn;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart';
|
|||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/gain_points.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/chat_floating_action_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/chat_view_background.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/input_bar_wrapper.dart';
|
||||
import 'package:fluffychat/utils/account_config.dart';
|
||||
import 'package:fluffychat/widgets/chat_settings_popup_menu.dart';
|
||||
|
|
@ -419,6 +420,9 @@ class ChatView extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
// #Pangea
|
||||
ChatViewBackground(
|
||||
choreographer: controller.choreographer,
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dar
|
|||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/mxc_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_highlighter/flutter_highlighter.dart';
|
||||
import 'package:flutter_highlighter/themes/shades-of-purple.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
|
|
@ -75,9 +74,6 @@ class HtmlMessage extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// #Pangea
|
||||
controller.textSelection.setMessageText(html);
|
||||
// Pangea#
|
||||
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||
|
||||
final linkColor = textColor.withAlpha(150);
|
||||
|
|
@ -97,9 +93,6 @@ class HtmlMessage extends StatelessWidget {
|
|||
// there is no need to pre-validate the html, as we validate it while rendering
|
||||
// #Pangea
|
||||
return SelectionArea(
|
||||
onSelectionChanged: (SelectedContent? selection) {
|
||||
controller.textSelection.onSelection(selection?.plainText);
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (pangeaMessageEvent != null && !isOverlay) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:fluffychat/pangea/enum/use_type.dart';
|
|||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/any_state_holder.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_buttons.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/utils/string_color.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
|
@ -39,7 +40,7 @@ class Message extends StatelessWidget {
|
|||
// #Pangea
|
||||
final bool immersionMode;
|
||||
final ChatController controller;
|
||||
final bool isOverlay;
|
||||
final MessageOverlayController? overlayController;
|
||||
// Pangea#
|
||||
final Color? avatarPresenceBackgroundColor;
|
||||
|
||||
|
|
@ -63,14 +64,15 @@ class Message extends StatelessWidget {
|
|||
// #Pangea
|
||||
required this.immersionMode,
|
||||
required this.controller,
|
||||
this.isOverlay = false,
|
||||
this.overlayController,
|
||||
// Pangea#
|
||||
super.key,
|
||||
});
|
||||
|
||||
// #Pangea
|
||||
void showToolbar(PangeaMessageEvent? pangeaMessageEvent) {
|
||||
if (pangeaMessageEvent != null && !isOverlay) {
|
||||
// if overlayController is not null, the message is already in overlay mode
|
||||
if (pangeaMessageEvent != null && overlayController == null) {
|
||||
controller.showToolbar(
|
||||
pangeaMessageEvent,
|
||||
nextEvent: nextEvent,
|
||||
|
|
@ -83,7 +85,6 @@ class Message extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// #Pangea
|
||||
debugPrint('Message.build()');
|
||||
PangeaMessageEvent? pangeaMessageEvent;
|
||||
if (event.type == EventTypes.Message) {
|
||||
pangeaMessageEvent = PangeaMessageEvent(
|
||||
|
|
@ -240,7 +241,9 @@ class Message extends StatelessWidget {
|
|||
// ),
|
||||
// )
|
||||
// else if (nextEventSameSender || ownMessage)
|
||||
if (nextEventSameSender || ownMessage || isOverlay)
|
||||
if (nextEventSameSender ||
|
||||
ownMessage ||
|
||||
overlayController != null)
|
||||
// Pangea#
|
||||
SizedBox(
|
||||
width: Avatar.defaultSize,
|
||||
|
|
@ -282,7 +285,8 @@ class Message extends StatelessWidget {
|
|||
children: [
|
||||
// #Pangea
|
||||
// if (!nextEventSameSender)
|
||||
if (!nextEventSameSender && !isOverlay)
|
||||
if (!nextEventSameSender &&
|
||||
overlayController == null)
|
||||
// Pangea#
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
|
|
@ -349,14 +353,14 @@ class Message extends StatelessWidget {
|
|||
),
|
||||
// #Pangea
|
||||
child: CompositedTransformTarget(
|
||||
link: isOverlay
|
||||
link: overlayController != null
|
||||
? LayerLinkAndKey('overlay_msg')
|
||||
.link
|
||||
: MatrixState.pAnyState
|
||||
.layerLinkAndKey(event.eventId)
|
||||
.link,
|
||||
child: Container(
|
||||
key: isOverlay
|
||||
key: overlayController != null
|
||||
? LayerLinkAndKey('overlay_msg')
|
||||
.key
|
||||
: MatrixState.pAnyState
|
||||
|
|
@ -449,7 +453,8 @@ class Message extends StatelessWidget {
|
|||
pangeaMessageEvent:
|
||||
pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
isOverlay: isOverlay,
|
||||
overlayController:
|
||||
overlayController,
|
||||
controller: controller,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: previousEvent,
|
||||
|
|
@ -537,7 +542,7 @@ class Message extends StatelessWidget {
|
|||
event.hasAggregatedEvents(timeline, RelationshipTypes.reaction);
|
||||
// #Pangea
|
||||
// if (showReceiptsRow || displayTime || selected || displayReadMarker) {
|
||||
if (!isOverlay &&
|
||||
if (overlayController == null &&
|
||||
(showReceiptsRow ||
|
||||
displayTime ||
|
||||
displayReadMarker ||
|
||||
|
|
@ -578,7 +583,7 @@ class Message extends StatelessWidget {
|
|||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
// #Pangea
|
||||
child: isOverlay ||
|
||||
child: overlayController != null ||
|
||||
(!showReceiptsRow &&
|
||||
!(pangeaMessageEvent?.showMessageButtons ?? false))
|
||||
// child: !showReceiptsRow
|
||||
|
|
@ -671,7 +676,7 @@ class Message extends StatelessWidget {
|
|||
top: nextEventSameSender ? 1.0 : 4.0,
|
||||
bottom:
|
||||
// #Pangea
|
||||
isOverlay
|
||||
overlayController != null
|
||||
? 0
|
||||
:
|
||||
// Pangea#
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import 'dart:math';
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/video_player.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar_selection_area.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_message_text.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/pangea_rich_text.dart';
|
||||
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
|
|
@ -35,7 +37,7 @@ class MessageContent extends StatelessWidget {
|
|||
//here rather than passing the choreographer? pangea rich text, a widget
|
||||
//further down in the chain is also using pangeaController so its not constant
|
||||
final bool immersionMode;
|
||||
final bool isOverlay;
|
||||
final MessageOverlayController? overlayController;
|
||||
final ChatController controller;
|
||||
final Event? nextEvent;
|
||||
final Event? prevEvent;
|
||||
|
|
@ -49,7 +51,7 @@ class MessageContent extends StatelessWidget {
|
|||
// #Pangea
|
||||
this.pangeaMessageEvent,
|
||||
required this.immersionMode,
|
||||
this.isOverlay = false,
|
||||
this.overlayController,
|
||||
required this.controller,
|
||||
this.nextEvent,
|
||||
this.prevEvent,
|
||||
|
|
@ -121,6 +123,7 @@ class MessageContent extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// debugger(when: overlayController != null);
|
||||
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
|
||||
final buttonTextColor = textColor;
|
||||
switch (event.type) {
|
||||
|
|
@ -208,7 +211,7 @@ class MessageContent extends StatelessWidget {
|
|||
textColor: textColor,
|
||||
room: event.room,
|
||||
// #Pangea
|
||||
isOverlay: isOverlay,
|
||||
isOverlay: overlayController != null,
|
||||
controller: controller,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
nextEvent: nextEvent,
|
||||
|
|
@ -303,26 +306,26 @@ class MessageContent extends StatelessWidget {
|
|||
decoration: event.redacted ? TextDecoration.lineThrough : null,
|
||||
height: 1.3,
|
||||
);
|
||||
|
||||
// debugger(when: overlayController != null);
|
||||
if (overlayController != null && pangeaMessageEvent != null) {
|
||||
return OverlayMessageText(
|
||||
pangeaMessageEvent: pangeaMessageEvent!,
|
||||
overlayController: overlayController!,
|
||||
);
|
||||
}
|
||||
|
||||
if (immersionMode && pangeaMessageEvent != null) {
|
||||
return Flexible(
|
||||
child: PangeaRichText(
|
||||
style: messageTextStyle,
|
||||
pangeaMessageEvent: pangeaMessageEvent!,
|
||||
immersionMode: immersionMode,
|
||||
isOverlay: isOverlay,
|
||||
isOverlay: overlayController != null,
|
||||
controller: controller,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isOverlay) {
|
||||
controller.textSelection.setMessageText(
|
||||
event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
hideReply: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
// Pangea#
|
||||
|
||||
return
|
||||
|
|
@ -330,7 +333,7 @@ class MessageContent extends StatelessWidget {
|
|||
ToolbarSelectionArea(
|
||||
controller: controller,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
isOverlay: isOverlay,
|
||||
isOverlay: overlayController != null,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
child:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fluffychat/pages/new_group/new_group_view.dart';
|
||||
import 'package:fluffychat/pangea/constants/bot_mode.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/chat_topic_model.dart';
|
||||
|
|
@ -108,7 +109,7 @@ class NewGroupController extends State<NewGroup> {
|
|||
final addBot = addConversationBotKey.currentState?.addBot ?? false;
|
||||
if (addBot) {
|
||||
final botOptions = addConversationBotKey.currentState!.botOptions;
|
||||
if (botOptions.mode == "custom") {
|
||||
if (botOptions.mode == BotMode.custom) {
|
||||
if (botOptions.customSystemPrompt == null ||
|
||||
botOptions.customSystemPrompt!.isEmpty) {
|
||||
setState(() {
|
||||
|
|
@ -118,7 +119,7 @@ class NewGroupController extends State<NewGroup> {
|
|||
});
|
||||
return;
|
||||
}
|
||||
} else if (botOptions.mode == "text_adventure") {
|
||||
} else if (botOptions.mode == BotMode.textAdventure) {
|
||||
if (botOptions.textAdventureGameMasterInstructions == null ||
|
||||
botOptions.textAdventureGameMasterInstructions!.isEmpty) {
|
||||
setState(() {
|
||||
|
|
|
|||
|
|
@ -33,45 +33,6 @@ class AlternativeTranslator {
|
|||
similarityResponse = null;
|
||||
}
|
||||
|
||||
// void onSeeAlternativeTranslationsTap() {
|
||||
// if (choreographer.itController.sourceText == null) {
|
||||
// ErrorHandler.logError(
|
||||
// m: "sourceText null in onSeeAlternativeTranslationsTap",
|
||||
// s: StackTrace.current,
|
||||
// );
|
||||
// choreographer.itController.closeIT();
|
||||
// return;
|
||||
// }
|
||||
// showAlternativeTranslations = true;
|
||||
// loadingAlternativeTranslations = true;
|
||||
// translate(choreographer.itController.sourceText!);
|
||||
// choreographer.setState();
|
||||
// }
|
||||
|
||||
// Future<void> translate(String text) async {
|
||||
// throw Exception('disabled translaations');
|
||||
// try {
|
||||
// final FullTextTranslationResponseModel results =
|
||||
// await FullTextTranslationRepo.translate(
|
||||
// accessToken: await choreographer.accessToken,
|
||||
// request: FullTextTranslationRequestModel(
|
||||
// text: text,
|
||||
// tgtLang: choreographer.l2LangCode!,
|
||||
// userL2: choreographer.l2LangCode!,
|
||||
// userL1: choreographer.l1LangCode!,
|
||||
// ),
|
||||
// );
|
||||
// // translations = results.translations;
|
||||
// } catch (err, stack) {
|
||||
// showAlternativeTranslations = false;
|
||||
// debugger(when: kDebugMode);
|
||||
// ErrorHandler.logError(e: err, s: stack);
|
||||
// } finally {
|
||||
// loadingAlternativeTranslations = false;
|
||||
// choreographer.setState();
|
||||
// }
|
||||
// }
|
||||
|
||||
Future<void> setTranslationFeedback() async {
|
||||
try {
|
||||
choreographer.startLoading();
|
||||
|
|
@ -155,20 +116,20 @@ class AlternativeTranslator {
|
|||
}
|
||||
switch (translationFeedbackKey) {
|
||||
case FeedbackKey.allCorrect:
|
||||
return "Score: 100%\n${L10n.of(context)!.allCorrect}";
|
||||
return "Match: 100%\n${L10n.of(context)!.allCorrect}";
|
||||
case FeedbackKey.newWayAllGood:
|
||||
return "Score: 100%\n${L10n.of(context)!.newWayAllGood}";
|
||||
return "Match: 100%\n${L10n.of(context)!.newWayAllGood}";
|
||||
case FeedbackKey.othersAreBetter:
|
||||
final num userScore =
|
||||
(similarityResponse!.userScore(userTranslation!) * 100).round();
|
||||
final String displayScore = userScore.toString();
|
||||
if (userScore > 90) {
|
||||
return "Score: $displayScore%\n${L10n.of(context)!.almostPerfect}";
|
||||
return "Match: $displayScore%\n${L10n.of(context)!.almostPerfect}";
|
||||
}
|
||||
if (userScore > 80) {
|
||||
return "Score: $displayScore%\n${L10n.of(context)!.prettyGood}";
|
||||
return "Match: $displayScore%\n${L10n.of(context)!.prettyGood}";
|
||||
}
|
||||
return "Score: $displayScore%\n${L10n.of(context)!.othersAreBetter}";
|
||||
return "Match: $displayScore%\n${L10n.of(context)!.othersAreBetter}";
|
||||
// case FeedbackKey.commonalityFeedback:
|
||||
// final int count = controller.completedITSteps
|
||||
// .where((element) => element.isCorrect)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ 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/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/tokens_event_content_model.dart';
|
||||
|
|
@ -103,11 +104,33 @@ class Choreographer {
|
|||
)
|
||||
: null;
|
||||
|
||||
// we've got a rather elaborate method of updating tokens after matches are accepted
|
||||
// so we need to check if the reconstructed text matches the current text
|
||||
// if not, let's get the tokens again and log an error
|
||||
if (igc.igcTextData?.tokens != null &&
|
||||
PangeaToken.reconstructText(igc.igcTextData!.tokens) != currentText) {
|
||||
if (kDebugMode) {
|
||||
PangeaToken.reconstructText(
|
||||
igc.igcTextData!.tokens,
|
||||
debugWalkThrough: true,
|
||||
);
|
||||
}
|
||||
ErrorHandler.logError(
|
||||
m: "reconstructed text not working",
|
||||
s: StackTrace.current,
|
||||
data: {
|
||||
"igcTextData": igc.igcTextData?.toJson(),
|
||||
"choreoRecord": choreoRecord.toJson(),
|
||||
},
|
||||
);
|
||||
await igc.getIGCTextData(onlyTokensAndLanguageDetection: true);
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ class IgcController {
|
|||
),
|
||||
roomId: choreographer.roomId,
|
||||
),
|
||||
cardSize: match.isITStart ? const Size(350, 260) : const Size(350, 400),
|
||||
cardSize: match.isITStart ? const Size(350, 260) : const Size(400, 400),
|
||||
transformTargetId: choreographer.inputTransformTargetKey,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,23 +56,10 @@ class ITController {
|
|||
choreographer.setState();
|
||||
}
|
||||
|
||||
bool _closingHint = false;
|
||||
Duration get animationSpeed => (_closingHint || !_willOpen)
|
||||
Duration get animationSpeed => (!_willOpen)
|
||||
? const Duration(milliseconds: 500)
|
||||
: const Duration(milliseconds: 2000);
|
||||
|
||||
void closeHint() {
|
||||
_closingHint = true;
|
||||
final String hintKey = InlineInstructions.translationChoices.toString();
|
||||
final instructionsController = choreographer.pangeaController.instructions;
|
||||
instructionsController.turnOffInstruction(hintKey);
|
||||
instructionsController.updateEnableInstructions(hintKey, true);
|
||||
choreographer.setState();
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
_closingHint = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> initializeIT(ITStartData itStartData) async {
|
||||
_willOpen = true;
|
||||
Future.delayed(const Duration(microseconds: 100), () {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
|
@ -8,16 +9,18 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
import '../../utils/bot_style.dart';
|
||||
import 'it_shimmer.dart';
|
||||
|
||||
typedef ChoiceCallback = void Function(String value, int index);
|
||||
|
||||
class ChoicesArray extends StatefulWidget {
|
||||
final bool isLoading;
|
||||
final List<Choice>? choices;
|
||||
final void Function(int) onPressed;
|
||||
final void Function(int)? onLongPress;
|
||||
final ChoiceCallback onPressed;
|
||||
final ChoiceCallback? onLongPress;
|
||||
final int? selectedChoiceIndex;
|
||||
final String originalSpan;
|
||||
final String Function(int) uniqueKeyForLayerLink;
|
||||
|
||||
/// some uses of this widget want to disable the choices
|
||||
/// some uses of this widget want to disable clicking of the choices
|
||||
final bool isActive;
|
||||
|
||||
const ChoicesArray({
|
||||
|
|
@ -63,24 +66,24 @@ class ChoicesArrayState extends State<ChoicesArray> {
|
|||
? ItShimmer(originalSpan: widget.originalSpan)
|
||||
: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: widget.choices
|
||||
?.asMap()
|
||||
.entries
|
||||
.map(
|
||||
(entry) => ChoiceItem(
|
||||
theme: theme,
|
||||
onLongPress:
|
||||
widget.isActive ? widget.onLongPress : null,
|
||||
onPressed: widget.isActive ? widget.onPressed : (_) {},
|
||||
entry: entry,
|
||||
interactionDisabled: interactionDisabled,
|
||||
enableInteraction: enableInteractions,
|
||||
disableInteraction: disableInteraction,
|
||||
isSelected: widget.selectedChoiceIndex == entry.key,
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
[],
|
||||
children: widget.choices!
|
||||
.mapIndexed(
|
||||
(index, entry) => ChoiceItem(
|
||||
theme: theme,
|
||||
onLongPress: widget.isActive ? widget.onLongPress : null,
|
||||
onPressed: widget.isActive
|
||||
? widget.onPressed
|
||||
: (String value, int index) {
|
||||
debugger(when: kDebugMode);
|
||||
},
|
||||
entry: MapEntry(index, entry),
|
||||
interactionDisabled: interactionDisabled,
|
||||
enableInteraction: enableInteractions,
|
||||
disableInteraction: disableInteraction,
|
||||
isSelected: widget.selectedChoiceIndex == index,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -112,8 +115,8 @@ class ChoiceItem extends StatelessWidget {
|
|||
|
||||
final MapEntry<int, Choice> entry;
|
||||
final ThemeData theme;
|
||||
final void Function(int p1)? onLongPress;
|
||||
final void Function(int p1) onPressed;
|
||||
final ChoiceCallback? onLongPress;
|
||||
final ChoiceCallback onPressed;
|
||||
final bool isSelected;
|
||||
final bool interactionDisabled;
|
||||
final VoidCallback enableInteraction;
|
||||
|
|
@ -136,27 +139,28 @@ class ChoiceItem extends StatelessWidget {
|
|||
child: Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: EdgeInsets.zero,
|
||||
decoration: isSelected
|
||||
? BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
border: Border.all(
|
||||
color: entry.value.color ?? theme.colorScheme.primary,
|
||||
style: BorderStyle.solid,
|
||||
width: 2.0,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? entry.value.color ?? theme.colorScheme.primary
|
||||
: Colors.transparent,
|
||||
style: BorderStyle.solid,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 7),
|
||||
),
|
||||
//if index is selected, then give the background a slight primary color
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
entry.value.color != null
|
||||
? entry.value.color!.withOpacity(0.2)
|
||||
: theme.colorScheme.primary.withOpacity(0.1),
|
||||
),
|
||||
backgroundColor: entry.value.color != null
|
||||
? WidgetStateProperty.all<Color>(
|
||||
entry.value.color!.withOpacity(0.2),
|
||||
)
|
||||
// : theme.colorScheme.primaryFixed,
|
||||
: null,
|
||||
textStyle: WidgetStateProperty.all(
|
||||
BotStyle.text(context),
|
||||
),
|
||||
|
|
@ -167,10 +171,11 @@ class ChoiceItem extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
onLongPress: onLongPress != null && !interactionDisabled
|
||||
? () => onLongPress!(entry.key)
|
||||
? () => onLongPress!(entry.value.text, entry.key)
|
||||
: null,
|
||||
onPressed:
|
||||
interactionDisabled ? null : () => onPressed(entry.key),
|
||||
onPressed: interactionDisabled
|
||||
? null
|
||||
: () => onPressed(entry.value.text, entry.key),
|
||||
child: Text(
|
||||
entry.value.text,
|
||||
style: BotStyle.text(context),
|
||||
|
|
|
|||
|
|
@ -49,12 +49,6 @@ class ITBarState extends State<ITBar> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
bool get instructionsTurnedOff =>
|
||||
widget.choreographer.pangeaController.instructions
|
||||
.wereInstructionsTurnedOff(
|
||||
InlineInstructions.translationChoices.toString(),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSize(
|
||||
|
|
@ -120,11 +114,12 @@ class ITBarState extends State<ITBar> {
|
|||
// const SizedBox(height: 40.0),
|
||||
OriginalText(controller: itController),
|
||||
const SizedBox(height: 7.0),
|
||||
if (!instructionsTurnedOff)
|
||||
if (!InstructionsEnum.translationChoices
|
||||
.toggledOff(context))
|
||||
InlineTooltip(
|
||||
body: InlineInstructions.translationChoices
|
||||
.body(context),
|
||||
onClose: itController.closeHint,
|
||||
instructionsEnum:
|
||||
InstructionsEnum.translationChoices,
|
||||
onClose: () => setState(() {}),
|
||||
),
|
||||
IntrinsicHeight(
|
||||
child: Container(
|
||||
|
|
@ -230,6 +225,7 @@ class OriginalText extends StatelessWidget {
|
|||
controller.sourceText != null
|
||||
? Flexible(child: Text(controller.sourceText!))
|
||||
: const LinearProgressIndicator(),
|
||||
const SizedBox(width: 4),
|
||||
if (controller.isEditingSourceText)
|
||||
Expanded(
|
||||
child: TextField(
|
||||
|
|
@ -248,7 +244,7 @@ class OriginalText extends StatelessWidget {
|
|||
if (!controller.isEditingSourceText && controller.sourceText != null)
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
opacity: controller.nextITStep != null ? 1.0 : 0.0,
|
||||
opacity: controller.nextITStep != null ? 0.7 : 0.0,
|
||||
child: IconButton(
|
||||
onPressed: () => {
|
||||
if (controller.nextITStep != null)
|
||||
|
|
@ -257,6 +253,7 @@ class OriginalText extends StatelessWidget {
|
|||
},
|
||||
},
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
iconSize: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -393,8 +390,8 @@ class ITChoices extends StatelessWidget {
|
|||
return Choice(text: "error", color: Colors.red);
|
||||
}
|
||||
}).toList(),
|
||||
onPressed: (int index) => selectContinuance(index, context),
|
||||
onLongPress: (int index) => showCard(context, index),
|
||||
onPressed: (value, index) => selectContinuance(index, context),
|
||||
onLongPress: (value, index) => showCard(context, index),
|
||||
uniqueKeyForLayerLink: (int index) => "itChoices$index",
|
||||
selectedChoiceIndex: null,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -131,11 +131,11 @@ class ITFeedbackCardView extends StatelessWidget {
|
|||
text: controller.widget.req.chosenContinuance,
|
||||
botExpression: BotExpression.nonGold,
|
||||
),
|
||||
Text(
|
||||
controller.widget.choiceFeedback,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Text(
|
||||
// controller.widget.choiceFeedback,
|
||||
// style: BotStyle.text(context),
|
||||
// ),
|
||||
const SizedBox(height: 10),
|
||||
if (controller.res == null)
|
||||
WhyButton(
|
||||
onPress: controller.handleGetExplanationButtonPress,
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class AlternativeTranslations extends StatelessWidget {
|
|||
Choice(text: controller.choreographer.altTranslator.translations.first),
|
||||
],
|
||||
// choices: controller.choreographer.altTranslator.translations,
|
||||
onPressed: (int index) {
|
||||
onPressed: (String value, int index) {
|
||||
controller.choreographer.onSelectAlternativeTranslation(
|
||||
controller.choreographer.altTranslator.translations[index],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
class AnalyticsConstants {
|
||||
static const int xpPerLevel = 2000;
|
||||
static const int xpPerLevel = 500;
|
||||
static const int vocabUseMaxXP = 30;
|
||||
static const int morphUseMaxXP = 500;
|
||||
}
|
||||
|
|
|
|||
6
lib/pangea/constants/bot_mode.dart
Normal file
6
lib/pangea/constants/bot_mode.dart
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
class BotMode {
|
||||
static const discussion = "discussion";
|
||||
static const custom = "custom";
|
||||
static const storyGame = "story_game";
|
||||
static const textAdventure = "text_adventure";
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:async';
|
||||
|
||||
class BaseController {
|
||||
final StreamController stateListener = StreamController();
|
||||
late Stream stateStream;
|
||||
class BaseController<T> {
|
||||
final StreamController<T> stateListener = StreamController<T>();
|
||||
late Stream<T> stateStream;
|
||||
|
||||
BaseController() {
|
||||
stateStream = stateListener.stream.asBroadcastStream();
|
||||
|
|
@ -12,7 +12,7 @@ class BaseController {
|
|||
stateListener.close();
|
||||
}
|
||||
|
||||
setState({dynamic data}) {
|
||||
setState(T data) {
|
||||
stateListener.add(data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class ClassController extends BaseController {
|
|||
}
|
||||
|
||||
setActiveSpaceIdInChatListController(String? classId) {
|
||||
setState(data: {"activeSpaceId": classId});
|
||||
setState({"activeSpaceId": classId});
|
||||
}
|
||||
|
||||
/// For all the spaces that the user is teaching, set the power levels
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/constants/match_rule_ids.dart';
|
||||
|
|
@ -42,7 +42,31 @@ class GetAnalyticsController {
|
|||
int get serverXP => currentXP - localXP;
|
||||
|
||||
/// Get the current level based on the number of xp points
|
||||
int get level => currentXP ~/ AnalyticsConstants.xpPerLevel;
|
||||
/// The formula is calculated from XP and modeled on RPG games
|
||||
int get level => 1 + sqrt((1 + 8 * currentXP / 100) / 2).floor();
|
||||
|
||||
// the minimum XP required for a given level
|
||||
double get minXPForLevel {
|
||||
return 12.5 * (2 * pow(level - 1, 2) - 1);
|
||||
}
|
||||
|
||||
// the minimum XP required for the next level
|
||||
double get minXPForNextLevel {
|
||||
return 12.5 * (2 * pow(level, 2) - 1);
|
||||
}
|
||||
|
||||
// the progress within the current level as a percentage (0.0 to 1.0)
|
||||
double get levelProgress {
|
||||
final progress =
|
||||
(currentXP - minXPForLevel) / (minXPForNextLevel - minXPForLevel);
|
||||
return progress >= 0 ? progress : 0;
|
||||
}
|
||||
|
||||
double get serverLevelProgress {
|
||||
final progress =
|
||||
(serverXP - minXPForLevel) / (minXPForNextLevel - minXPForLevel);
|
||||
return progress >= 0 ? progress : 0;
|
||||
}
|
||||
|
||||
void initialize() {
|
||||
_analyticsUpdateSubscription ??= _pangeaController
|
||||
|
|
|
|||
|
|
@ -1,285 +1,145 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/base_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/tokens_repo.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../constants/pangea_event_types.dart';
|
||||
import '../enum/use_type.dart';
|
||||
import '../models/choreo_record.dart';
|
||||
import '../repo/full_text_translation_repo.dart';
|
||||
import '../utils/error_handler.dart';
|
||||
|
||||
// TODO - make this static and take it out of the _pangeaController
|
||||
// will need to pass accessToken to the requests
|
||||
class MessageDataController extends BaseController {
|
||||
late PangeaController _pangeaController;
|
||||
|
||||
final List<CacheItem> _cache = [];
|
||||
final List<RepresentationCacheItem> _representationCache = [];
|
||||
final Map<int, Future<List<PangeaToken>>> _tokensCache = {};
|
||||
final Map<int, Future<PangeaRepresentation>> _representationCache = {};
|
||||
late Timer _cacheTimer;
|
||||
|
||||
MessageDataController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
_startCacheTimer();
|
||||
}
|
||||
|
||||
CacheItem? getItem(String parentId, String type, String langCode) =>
|
||||
_cache.firstWhereOrNull(
|
||||
(e) =>
|
||||
e.parentId == parentId && e.type == type && e.langCode == langCode,
|
||||
);
|
||||
|
||||
RepresentationCacheItem? getRepresentationCacheItem(
|
||||
String parentId,
|
||||
String langCode,
|
||||
) =>
|
||||
_representationCache.firstWhereOrNull(
|
||||
(e) => e.parentId == parentId && e.langCode == langCode,
|
||||
);
|
||||
|
||||
Future<PangeaMessageTokens?> _getTokens(
|
||||
TokensRequestModel req,
|
||||
) async {
|
||||
final accessToken = _pangeaController.userController.accessToken;
|
||||
|
||||
final TokensResponseModel igcTextData =
|
||||
await TokensRepo.tokenize(accessToken, req);
|
||||
|
||||
return PangeaMessageTokens(tokens: igcTextData.tokens);
|
||||
/// Starts a timer that clears the cache every 10 minutes
|
||||
void _startCacheTimer() {
|
||||
_cacheTimer = Timer.periodic(const Duration(minutes: 10), (timer) {
|
||||
_clearCache();
|
||||
});
|
||||
}
|
||||
|
||||
Future<Event?> _getTokenEvent({
|
||||
required BuildContext context,
|
||||
required String repEventId,
|
||||
/// Clears the token and representation caches
|
||||
void _clearCache() {
|
||||
_tokensCache.clear();
|
||||
_representationCache.clear();
|
||||
debugPrint("message data cache cleared.");
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cacheTimer.cancel(); // Cancel the timer when the controller is disposed
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// get tokens from the server
|
||||
/// if repEventId is not null, send the tokens to the room
|
||||
Future<List<PangeaToken>> _getTokens({
|
||||
required String? repEventId,
|
||||
required TokensRequestModel req,
|
||||
required Room room,
|
||||
required Room? room,
|
||||
}) async {
|
||||
try {
|
||||
final PangeaMessageTokens? pangeaMessageTokens = await _getTokens(
|
||||
req,
|
||||
);
|
||||
if (pangeaMessageTokens == null) return null;
|
||||
|
||||
final Event? tokensEvent = await room.sendPangeaEvent(
|
||||
content: pangeaMessageTokens.toJson(),
|
||||
parentEventId: repEventId,
|
||||
type: PangeaEventTypes.tokens,
|
||||
);
|
||||
|
||||
return tokensEvent;
|
||||
} catch (err, stack) {
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message: "err in _getTokenEvent with repEventId $repEventId",
|
||||
),
|
||||
);
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb.fromJson({"req": req.toJson()}),
|
||||
);
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb.fromJson({"room": room.toJson()}),
|
||||
);
|
||||
ErrorHandler.logError(e: err, s: stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Event?> getTokenEvent({
|
||||
required BuildContext context,
|
||||
required String repEventId,
|
||||
required TokensRequestModel req,
|
||||
required Room room,
|
||||
}) async {
|
||||
final CacheItem? item =
|
||||
getItem(repEventId, PangeaEventTypes.tokens, req.userL2);
|
||||
if (item != null) return item.data;
|
||||
|
||||
_cache.add(
|
||||
CacheItem(
|
||||
repEventId,
|
||||
PangeaEventTypes.tokens,
|
||||
req.userL2,
|
||||
_getTokenEvent(
|
||||
context: context,
|
||||
repEventId: repEventId,
|
||||
req: req,
|
||||
room: room,
|
||||
),
|
||||
),
|
||||
final TokensResponseModel res = await TokensRepo.tokenize(
|
||||
_pangeaController.userController.accessToken,
|
||||
req,
|
||||
);
|
||||
if (repEventId != null && room != null) {
|
||||
room
|
||||
.sendPangeaEvent(
|
||||
content: PangeaMessageTokens(tokens: res.tokens).toJson(),
|
||||
parentEventId: repEventId,
|
||||
type: PangeaEventTypes.tokens,
|
||||
)
|
||||
.catchError(
|
||||
(e) => ErrorHandler.logError(
|
||||
m: "error in _getTokens.sendPangeaEvent",
|
||||
e: e,
|
||||
s: StackTrace.current,
|
||||
data: req.toJson(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _cache.last.data;
|
||||
return res.tokens;
|
||||
}
|
||||
|
||||
/// get tokens from the server
|
||||
/// first check if the tokens are in the cache
|
||||
/// if repEventId is not null, send the tokens to the room
|
||||
Future<List<PangeaToken>> getTokens({
|
||||
required String? repEventId,
|
||||
required TokensRequestModel req,
|
||||
required Room? room,
|
||||
}) =>
|
||||
_tokensCache[req.hashCode] ??= _getTokens(
|
||||
repEventId: repEventId,
|
||||
req: req,
|
||||
room: room,
|
||||
);
|
||||
|
||||
/////// translation ////////
|
||||
|
||||
/// make representation (originalSent and originalWritten always false)
|
||||
Future<Event?> _sendRepresentationMatrixEvent({
|
||||
required PangeaRepresentation representation,
|
||||
required String messageEventId,
|
||||
required Room room,
|
||||
/// get translation from the server
|
||||
/// if in cache, return from cache
|
||||
/// if not in cache, get from server
|
||||
/// send the translation to the room as a representation event
|
||||
Future<PangeaRepresentation> getPangeaRepresentation({
|
||||
required FullTextTranslationRequestModel req,
|
||||
required Event messageEvent,
|
||||
}) async {
|
||||
try {
|
||||
final Event? repEvent = await room.sendPangeaEvent(
|
||||
content: representation.toJson(),
|
||||
parentEventId: messageEventId,
|
||||
type: PangeaEventTypes.representation,
|
||||
);
|
||||
|
||||
return repEvent;
|
||||
} catch (err, stack) {
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
message:
|
||||
"err in _sendRepresentationMatrixEvent with messageEventId $messageEventId",
|
||||
),
|
||||
);
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb.fromJson({"room": room.toJson()}),
|
||||
);
|
||||
ErrorHandler.logError(e: err, s: stack);
|
||||
return null;
|
||||
}
|
||||
return _representationCache[req.hashCode] ??=
|
||||
_getPangeaRepresentation(req: req, messageEvent: messageEvent);
|
||||
}
|
||||
|
||||
Future<PangeaRepresentation?> getPangeaRepresentation({
|
||||
required String text,
|
||||
required String? source,
|
||||
required String target,
|
||||
required Room room,
|
||||
Future<PangeaRepresentation> _getPangeaRepresentation({
|
||||
required FullTextTranslationRequestModel req,
|
||||
required Event messageEvent,
|
||||
}) async {
|
||||
final RepresentationCacheItem? item =
|
||||
getRepresentationCacheItem(text, target);
|
||||
if (item != null) return item.data;
|
||||
|
||||
_representationCache.add(
|
||||
RepresentationCacheItem(
|
||||
text,
|
||||
target,
|
||||
_getPangeaRepresentation(
|
||||
text: text,
|
||||
source: source,
|
||||
target: target,
|
||||
room: room,
|
||||
),
|
||||
),
|
||||
final FullTextTranslationResponseModel res =
|
||||
await FullTextTranslationRepo.translate(
|
||||
accessToken: _pangeaController.userController.accessToken,
|
||||
request: req,
|
||||
);
|
||||
|
||||
return _representationCache.last.data;
|
||||
}
|
||||
|
||||
Future<PangeaRepresentation?> _getPangeaRepresentation({
|
||||
required String text,
|
||||
required String? source,
|
||||
required String target,
|
||||
required Room room,
|
||||
}) async {
|
||||
if (_pangeaController.languageController.userL2 == null ||
|
||||
_pangeaController.languageController.userL1 == null) {
|
||||
ErrorHandler.logError(
|
||||
e: "userL1 or userL2 is null in _getPangeaRepresentation",
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
final req = FullTextTranslationRequestModel(
|
||||
text: text,
|
||||
tgtLang: target,
|
||||
srcLang: source,
|
||||
userL2: _pangeaController.languageController.userL2!.langCode,
|
||||
userL1: _pangeaController.languageController.userL1!.langCode,
|
||||
final rep = PangeaRepresentation(
|
||||
langCode: req.tgtLang,
|
||||
text: res.bestTranslation,
|
||||
originalSent: false,
|
||||
originalWritten: false,
|
||||
);
|
||||
|
||||
try {
|
||||
final FullTextTranslationResponseModel res =
|
||||
await FullTextTranslationRepo.translate(
|
||||
accessToken: _pangeaController.userController.accessToken,
|
||||
request: req,
|
||||
);
|
||||
messageEvent.room
|
||||
.sendPangeaEvent(
|
||||
content: rep.toJson(),
|
||||
parentEventId: messageEvent.eventId,
|
||||
type: PangeaEventTypes.representation,
|
||||
)
|
||||
.catchError(
|
||||
(e) => ErrorHandler.logError(
|
||||
m: "error in _getPangeaRepresentation.sendPangeaEvent",
|
||||
e: e,
|
||||
s: StackTrace.current,
|
||||
data: req.toJson(),
|
||||
),
|
||||
);
|
||||
|
||||
return PangeaRepresentation(
|
||||
langCode: req.tgtLang,
|
||||
text: res.bestTranslation,
|
||||
originalSent: false,
|
||||
originalWritten: false,
|
||||
);
|
||||
} catch (err, stack) {
|
||||
ErrorHandler.logError(e: err, s: stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// make representation (originalSent and originalWritten always false)
|
||||
Future<Event?> sendRepresentationMatrixEvent({
|
||||
required PangeaRepresentation representation,
|
||||
required String messageEventId,
|
||||
required Room room,
|
||||
required String target,
|
||||
}) async {
|
||||
final CacheItem? item =
|
||||
getItem(messageEventId, PangeaEventTypes.representation, target);
|
||||
if (item != null) return item.data;
|
||||
|
||||
_cache.add(
|
||||
CacheItem(
|
||||
messageEventId,
|
||||
PangeaEventTypes.representation,
|
||||
target,
|
||||
_sendRepresentationMatrixEvent(
|
||||
messageEventId: messageEventId,
|
||||
room: room,
|
||||
representation: representation,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return _cache.last.data;
|
||||
return rep;
|
||||
}
|
||||
}
|
||||
|
||||
class MessageDataQueueItem {
|
||||
String transactionId;
|
||||
|
||||
List<RepTokensAndRecord> repTokensAndRecords;
|
||||
|
||||
UseType useType;
|
||||
|
||||
MessageDataQueueItem(
|
||||
this.transactionId,
|
||||
this.repTokensAndRecords,
|
||||
this.useType,
|
||||
// required this.recentMessageRecord,
|
||||
);
|
||||
}
|
||||
|
||||
class RepTokensAndRecord {
|
||||
PangeaRepresentation representation;
|
||||
ChoreoRecord? choreoRecord;
|
||||
PangeaMessageTokens? tokens;
|
||||
RepTokensAndRecord(this.representation, this.choreoRecord, this.tokens);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"rep": representation.toJson(),
|
||||
"choreoRecord": choreoRecord?.toJson(),
|
||||
"tokens": tokens?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
class CacheItem {
|
||||
String parentId;
|
||||
String langCode;
|
||||
String type;
|
||||
Future<Event?> data;
|
||||
|
||||
CacheItem(this.parentId, this.type, this.langCode, this.data);
|
||||
}
|
||||
|
||||
class RepresentationCacheItem {
|
||||
String parentId;
|
||||
String langCode;
|
||||
Future<PangeaRepresentation?> data;
|
||||
|
||||
RepresentationCacheItem(this.parentId, this.langCode, this.data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
import 'dart:async';
|
||||
|
||||
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/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_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/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/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -25,11 +19,11 @@ enum AnalyticsUpdateType { server, local }
|
|||
/// 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 extends BaseController {
|
||||
class MyAnalyticsController extends BaseController<AnalyticsStream> {
|
||||
late PangeaController _pangeaController;
|
||||
CachedStreamController<AnalyticsUpdateType> analyticsUpdateStream =
|
||||
CachedStreamController<AnalyticsUpdateType>();
|
||||
StreamSubscription? _messageSendSubscription;
|
||||
StreamSubscription<AnalyticsStream>? _analyticsStream;
|
||||
Timer? _updateTimer;
|
||||
|
||||
Client get _client => _pangeaController.matrixState.client;
|
||||
|
|
@ -60,9 +54,8 @@ class MyAnalyticsController extends BaseController {
|
|||
void initialize() {
|
||||
// Listen to a stream that provides the eventIDs
|
||||
// of new messages sent by the logged in user
|
||||
_messageSendSubscription ??= stateStream
|
||||
.where((data) => data is Map)
|
||||
.listen((data) => onMessageSent(data as Map<String, dynamic>));
|
||||
_analyticsStream ??=
|
||||
stateStream.listen((data) => _onNewAnalyticsData(data));
|
||||
|
||||
_refreshAnalyticsIfOutdated();
|
||||
}
|
||||
|
|
@ -73,8 +66,8 @@ class MyAnalyticsController extends BaseController {
|
|||
_updateTimer?.cancel();
|
||||
lastUpdated = null;
|
||||
lastUpdatedCompleter = Completer<DateTime?>();
|
||||
_messageSendSubscription?.cancel();
|
||||
_messageSendSubscription = null;
|
||||
_analyticsStream?.cancel();
|
||||
_analyticsStream = null;
|
||||
_refreshAnalyticsIfOutdated();
|
||||
clearMessagesSinceUpdate();
|
||||
}
|
||||
|
|
@ -103,77 +96,36 @@ class MyAnalyticsController extends BaseController {
|
|||
final DateTime yesterday = DateTime.now().subtract(_timeSinceUpdate);
|
||||
if (lastUpdated?.isBefore(yesterday) ?? true) {
|
||||
debugPrint("analytics out-of-date, updating");
|
||||
await updateAnalytics();
|
||||
await sendLocalAnalyticsToAnalyticsRoom();
|
||||
}
|
||||
}
|
||||
|
||||
/// Given the data from a newly sent message, format and cache
|
||||
/// the message's construct data locally and reset the update timer
|
||||
void onMessageSent(Map<String, dynamic> data) {
|
||||
// cancel the last timer that was set on message event and
|
||||
// reset it to fire after _minutesBeforeUpdate minutes
|
||||
_updateTimer?.cancel();
|
||||
_updateTimer = Timer(Duration(minutes: _minutesBeforeUpdate), () {
|
||||
debugPrint("timer fired, updating analytics");
|
||||
updateAnalytics();
|
||||
});
|
||||
void _onNewAnalyticsData(AnalyticsStream data) {
|
||||
final List<OneConstructUse> constructs = _getDraftUses(data.roomId);
|
||||
|
||||
// extract the relevant data about this message
|
||||
final String? eventID = data['eventID'];
|
||||
final String? roomID = data['roomID'];
|
||||
final String? eventType = data['eventType'];
|
||||
final PangeaRepresentation? originalSent = data['originalSent'];
|
||||
final PangeaMessageTokens? tokensSent = data['tokensSent'];
|
||||
final ChoreoRecord? choreo = data['choreo'];
|
||||
final PracticeActivityEvent? practiceActivity = data['practiceActivity'];
|
||||
final PracticeActivityRecordModel? recordModel = data['recordModel'];
|
||||
constructs.addAll(data.constructs);
|
||||
|
||||
if (roomID == null || eventID == null) return;
|
||||
|
||||
// convert that data into construct uses and add it to the cache
|
||||
final metadata = ConstructUseMetaData(
|
||||
roomId: roomID,
|
||||
eventId: eventID,
|
||||
timeStamp: DateTime.now(),
|
||||
);
|
||||
|
||||
final List<OneConstructUse> constructs = getDraftUses(roomID);
|
||||
|
||||
if (eventType == EventTypes.Message) {
|
||||
final grammarConstructs =
|
||||
choreo?.grammarConstructUses(metadata: metadata);
|
||||
final vocabUses = tokensSent != null
|
||||
? originalSent?.vocabUses(
|
||||
choreo: choreo,
|
||||
tokens: tokensSent.tokens,
|
||||
metadata: metadata,
|
||||
)
|
||||
: null;
|
||||
constructs.addAll([
|
||||
...(grammarConstructs ?? []),
|
||||
...(vocabUses ?? []),
|
||||
]);
|
||||
}
|
||||
|
||||
if (eventType == PangeaEventTypes.activityRecord &&
|
||||
practiceActivity != null) {
|
||||
final activityConstructs = recordModel?.uses(
|
||||
practiceActivity,
|
||||
metadata: metadata,
|
||||
);
|
||||
constructs.addAll(activityConstructs ?? []);
|
||||
}
|
||||
final String eventID = data.eventId;
|
||||
final String roomID = data.roomId;
|
||||
|
||||
_pangeaController.analytics
|
||||
.filterConstructs(unfilteredConstructs: constructs)
|
||||
.then((filtered) {
|
||||
for (final use in filtered) {
|
||||
debugPrint(
|
||||
"_onNewAnalyticsData filtered use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}",
|
||||
);
|
||||
}
|
||||
if (filtered.isEmpty) return;
|
||||
filtered.addAll(getDraftUses(roomID));
|
||||
|
||||
final level = _pangeaController.analytics.level;
|
||||
addLocalMessage(eventID, filtered).then(
|
||||
|
||||
_addLocalMessage(eventID, filtered).then(
|
||||
(_) {
|
||||
clearDraftUses(roomID);
|
||||
afterAddLocalMessages(level);
|
||||
_clearDraftUses(roomID);
|
||||
_decideWhetherToUpdateAnalyticsRoom(level);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -216,40 +168,48 @@ class MyAnalyticsController extends BaseController {
|
|||
}
|
||||
}
|
||||
|
||||
if (kDebugMode) {
|
||||
for (final use in uses) {
|
||||
debugPrint(
|
||||
"Draft use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final level = _pangeaController.analytics.level;
|
||||
addLocalMessage('draft$roomID', uses).then(
|
||||
(_) => afterAddLocalMessages(level),
|
||||
_addLocalMessage('draft$roomID', uses).then(
|
||||
(_) => _decideWhetherToUpdateAnalyticsRoom(level),
|
||||
);
|
||||
}
|
||||
|
||||
List<OneConstructUse> getDraftUses(String roomID) {
|
||||
List<OneConstructUse> _getDraftUses(String roomID) {
|
||||
final currentCache = _pangeaController.analytics.messagesSinceUpdate;
|
||||
return currentCache['draft$roomID'] ?? [];
|
||||
}
|
||||
|
||||
void clearDraftUses(String roomID) {
|
||||
void _clearDraftUses(String roomID) {
|
||||
final currentCache = _pangeaController.analytics.messagesSinceUpdate;
|
||||
currentCache.remove('draft$roomID');
|
||||
setMessagesSinceUpdate(currentCache);
|
||||
_setMessagesSinceUpdate(currentCache);
|
||||
}
|
||||
|
||||
/// Add a list of construct uses for a new message to the local
|
||||
/// cache of recently sent messages
|
||||
Future<void> addLocalMessage(
|
||||
String eventID,
|
||||
Future<void> _addLocalMessage(
|
||||
String cacheKey,
|
||||
List<OneConstructUse> constructs,
|
||||
) async {
|
||||
try {
|
||||
final currentCache = _pangeaController.analytics.messagesSinceUpdate;
|
||||
constructs.addAll(currentCache[eventID] ?? []);
|
||||
currentCache[eventID] = constructs;
|
||||
constructs.addAll(currentCache[cacheKey] ?? []);
|
||||
currentCache[cacheKey] = constructs;
|
||||
|
||||
await setMessagesSinceUpdate(currentCache);
|
||||
await _setMessagesSinceUpdate(currentCache);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: PangeaWarningError("Failed to add message since update: $e"),
|
||||
s: s,
|
||||
m: 'Failed to add message since update for eventId: $eventID',
|
||||
m: 'Failed to add message since update for eventId: $cacheKey',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -258,27 +218,46 @@ class MyAnalyticsController extends BaseController {
|
|||
/// If the addition brought the total number of messages in the cache
|
||||
/// to the max, or if the addition triggered a level-up, update the analytics.
|
||||
/// Otherwise, add a local update to the alert stream.
|
||||
void afterAddLocalMessages(int prevLevel) {
|
||||
void _decideWhetherToUpdateAnalyticsRoom(int prevLevel) {
|
||||
// cancel the last timer that was set on message event and
|
||||
// reset it to fire after _minutesBeforeUpdate minutes
|
||||
_updateTimer?.cancel();
|
||||
_updateTimer = Timer(Duration(minutes: _minutesBeforeUpdate), () {
|
||||
debugPrint("timer fired, updating analytics");
|
||||
sendLocalAnalyticsToAnalyticsRoom();
|
||||
});
|
||||
|
||||
if (_pangeaController.analytics.messagesSinceUpdate.length >
|
||||
_maxMessagesCached) {
|
||||
debugPrint("reached max messages, updating");
|
||||
updateAnalytics();
|
||||
sendLocalAnalyticsToAnalyticsRoom();
|
||||
return;
|
||||
}
|
||||
|
||||
final int newLevel = _pangeaController.analytics.level;
|
||||
newLevel > prevLevel
|
||||
? updateAnalytics()
|
||||
? sendLocalAnalyticsToAnalyticsRoom()
|
||||
: analyticsUpdateStream.add(AnalyticsUpdateType.local);
|
||||
}
|
||||
|
||||
/// Clears the local cache of recently sent constructs. Called before updating analytics
|
||||
void clearMessagesSinceUpdate() {
|
||||
_pangeaController.pStoreService.delete(PLocalKey.messagesSinceUpdate);
|
||||
final localCache = _pangeaController.analytics.messagesSinceUpdate;
|
||||
final draftKeys = localCache.keys.where((key) => key.startsWith('draft'));
|
||||
if (draftKeys.isEmpty) {
|
||||
_pangeaController.pStoreService.delete(PLocalKey.messagesSinceUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
final Map<String, List<OneConstructUse>> newCache = {};
|
||||
for (final key in draftKeys) {
|
||||
newCache[key] = localCache[key]!;
|
||||
}
|
||||
_setMessagesSinceUpdate(newCache);
|
||||
}
|
||||
|
||||
/// Save the local cache of recently sent constructs to the local storage
|
||||
Future<void> setMessagesSinceUpdate(
|
||||
Future<void> _setMessagesSinceUpdate(
|
||||
Map<String, List<OneConstructUse>> cache,
|
||||
) async {
|
||||
final formattedCache = {};
|
||||
|
|
@ -302,7 +281,7 @@ class MyAnalyticsController extends BaseController {
|
|||
/// for the completion of the previous update and returns. Otherwise, it creates a new [_updateCompleter] and
|
||||
/// proceeds with the update process. If the update is successful, it clears any messages that were received
|
||||
/// since the last update and notifies the [analyticsUpdateStream].
|
||||
Future<void> updateAnalytics() async {
|
||||
Future<void> sendLocalAnalyticsToAnalyticsRoom() async {
|
||||
if (_pangeaController.matrixState.client.userID == null) return;
|
||||
if (!(_updateCompleter?.isCompleted ?? true)) {
|
||||
await _updateCompleter!.future;
|
||||
|
|
@ -348,3 +327,16 @@ class MyAnalyticsController extends BaseController {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsStream {
|
||||
final String eventId;
|
||||
final String roomId;
|
||||
|
||||
final List<OneConstructUse> constructs;
|
||||
|
||||
AnalyticsStream({
|
||||
required this.eventId,
|
||||
required this.roomId,
|
||||
required this.constructs,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class PangeaController {
|
|||
speechToText = SpeechToTextController(this);
|
||||
languageDetection = LanguageDetectionController(this);
|
||||
activityRecordController = PracticeActivityRecordController(this);
|
||||
practiceGenerationController = PracticeGenerationController();
|
||||
practiceGenerationController = PracticeGenerationController(this);
|
||||
PAuthGaurd.pController = this;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,27 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.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/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/network/requests.dart';
|
||||
import 'package:fluffychat/pangea/network/urls.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// Represents an item in the completion cache.
|
||||
class _RequestCacheItem {
|
||||
PracticeActivityRequest req;
|
||||
MessageActivityRequest req;
|
||||
|
||||
Future<PracticeActivityEvent?> practiceActivityEvent;
|
||||
|
||||
|
|
@ -27,7 +36,10 @@ class PracticeGenerationController {
|
|||
static final Map<int, _RequestCacheItem> _cache = {};
|
||||
Timer? _cacheClearTimer;
|
||||
|
||||
PracticeGenerationController() {
|
||||
late PangeaController _pangeaController;
|
||||
|
||||
PracticeGenerationController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
_initializeCacheClearing();
|
||||
}
|
||||
|
||||
|
|
@ -64,8 +76,35 @@ class PracticeGenerationController {
|
|||
);
|
||||
}
|
||||
|
||||
Future<MessageActivityResponse> _fetch({
|
||||
required String accessToken,
|
||||
required MessageActivityRequest requestModel,
|
||||
}) async {
|
||||
final Requests request = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: accessToken,
|
||||
);
|
||||
final Response res = await request.post(
|
||||
url: PApiUrls.messageActivityGeneration,
|
||||
body: requestModel.toJson(),
|
||||
);
|
||||
|
||||
if (res.statusCode == 200) {
|
||||
final Map<String, dynamic> json = jsonDecode(utf8.decode(res.bodyBytes));
|
||||
|
||||
final response = MessageActivityResponse.fromJson(json);
|
||||
|
||||
return response;
|
||||
} else {
|
||||
debugger(when: kDebugMode);
|
||||
throw Exception('Failed to convert speech to text');
|
||||
}
|
||||
}
|
||||
|
||||
//TODO - allow return of activity content before sending the event
|
||||
// this requires some downstream changes to the way the event is handled
|
||||
Future<PracticeActivityEvent?> getPracticeActivity(
|
||||
PracticeActivityRequest req,
|
||||
MessageActivityRequest req,
|
||||
PangeaMessageEvent event,
|
||||
) async {
|
||||
final int cacheKey = req.hashCode;
|
||||
|
|
@ -75,8 +114,36 @@ class PracticeGenerationController {
|
|||
} else {
|
||||
//TODO - send request to server/bot, either via API or via event of type pangeaActivityReq
|
||||
// for now, just make and send the event from the client
|
||||
final MessageActivityResponse res = await _fetch(
|
||||
accessToken: _pangeaController.userController.accessToken,
|
||||
requestModel: req,
|
||||
);
|
||||
|
||||
// if the server points to an existing event, return that event
|
||||
if (res.existingActivityEventId != null) {
|
||||
final Event? existingEvent =
|
||||
await event.room.getEventById(res.existingActivityEventId!);
|
||||
|
||||
debugPrint(
|
||||
'Existing activity event found: ${existingEvent?.content}',
|
||||
);
|
||||
if (existingEvent != null) {
|
||||
return PracticeActivityEvent(
|
||||
event: existingEvent,
|
||||
timeline: event.timeline,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (res.activity == null) {
|
||||
debugPrint('No activity generated');
|
||||
return null;
|
||||
}
|
||||
|
||||
debugPrint('Activity generated: ${res.activity!.toJson()}');
|
||||
|
||||
final Future<PracticeActivityEvent?> eventFuture =
|
||||
_sendAndPackageEvent(dummyModel(event), event);
|
||||
_sendAndPackageEvent(res.activity!, event);
|
||||
|
||||
_cache[cacheKey] =
|
||||
_RequestCacheItem(req: req, practiceActivityEvent: eventFuture);
|
||||
|
|
@ -85,7 +152,7 @@ class PracticeGenerationController {
|
|||
}
|
||||
}
|
||||
|
||||
PracticeActivityModel dummyModel(PangeaMessageEvent event) =>
|
||||
PracticeActivityModel _dummyModel(PangeaMessageEvent event) =>
|
||||
PracticeActivityModel(
|
||||
tgtConstructs: [
|
||||
ConstructIdentifier(lemma: "be", type: ConstructTypeEnum.vocab),
|
||||
|
|
@ -97,6 +164,7 @@ class PracticeGenerationController {
|
|||
question: "What is a synonym for 'happy'?",
|
||||
choices: ["sad", "angry", "joyful", "tired"],
|
||||
answer: "joyful",
|
||||
spanDisplayDetails: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ class SubscriptionController extends BaseController {
|
|||
}
|
||||
}
|
||||
}
|
||||
setState();
|
||||
setState(null);
|
||||
} catch (e, s) {
|
||||
debugPrint("Failed to initialize subscription controller");
|
||||
ErrorHandler.logError(e: e, s: s);
|
||||
|
|
@ -140,7 +140,7 @@ class SubscriptionController extends BaseController {
|
|||
PLocalKey.beganWebPayment,
|
||||
true,
|
||||
);
|
||||
setState();
|
||||
setState(null);
|
||||
launchUrlString(
|
||||
paymentLink,
|
||||
webOnlyWindowName: "_self",
|
||||
|
|
@ -224,7 +224,7 @@ class SubscriptionController extends BaseController {
|
|||
return;
|
||||
}
|
||||
await subscription!.setCustomerInfo();
|
||||
setState();
|
||||
setState(null);
|
||||
}
|
||||
|
||||
CanSendStatus get canSendStatus => isSubscribed
|
||||
|
|
@ -254,7 +254,7 @@ class SubscriptionController extends BaseController {
|
|||
!isSubscribed &&
|
||||
(_lastDismissedPaywall == null ||
|
||||
DateTime.now().difference(_lastDismissedPaywall!).inHours >
|
||||
(24 * (_paywallBackoff ?? 1)));
|
||||
(1 * (_paywallBackoff ?? 1)));
|
||||
}
|
||||
|
||||
void dismissPaywall() async {
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class WordController extends BaseController {
|
|||
if (local == null) {
|
||||
if (_wordData.length > 100) _wordData.clear();
|
||||
_wordData.add(w);
|
||||
setState();
|
||||
setState(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,13 +29,14 @@ enum ConstructUseTypeEnum {
|
|||
/// encountered as distractor in IGC flow and selected it
|
||||
incIGC,
|
||||
|
||||
/// selected correctly in practice activity flow
|
||||
/// selected correctly in word meaning in context practice activity
|
||||
corPA,
|
||||
|
||||
/// encountered as distractor in practice activity flow and correctly ignored it
|
||||
/// encountered as distractor in word meaning in context practice activity and correctly ignored it
|
||||
/// Currently not used
|
||||
ignPA,
|
||||
|
||||
/// was target construct in practice activity but user did not select correctly
|
||||
/// was target construct in word meaning in context practice activity and incorrectly selected
|
||||
incPA,
|
||||
}
|
||||
|
||||
|
|
@ -125,9 +126,9 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum {
|
|||
case ConstructUseTypeEnum.unk:
|
||||
return 0;
|
||||
case ConstructUseTypeEnum.corPA:
|
||||
return 2;
|
||||
return 5;
|
||||
case ConstructUseTypeEnum.incPA:
|
||||
return -1;
|
||||
return -2;
|
||||
case ConstructUseTypeEnum.ignPA:
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.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';
|
||||
|
||||
|
|
@ -8,10 +12,22 @@ enum InstructionsEnum {
|
|||
clickMessage,
|
||||
blurMeansTranslate,
|
||||
tooltipInstructions,
|
||||
speechToText,
|
||||
l1Translation,
|
||||
translationChoices,
|
||||
clickAgainToDeselect,
|
||||
}
|
||||
|
||||
extension InstructionsEnumExtension on InstructionsEnum {
|
||||
String title(BuildContext context) {
|
||||
if (!context.mounted) {
|
||||
ErrorHandler.logError(
|
||||
e: Exception("Context not mounted"),
|
||||
m: 'InstructionsEnumExtension.title for $this',
|
||||
);
|
||||
debugger(when: kDebugMode);
|
||||
return '';
|
||||
}
|
||||
switch (this) {
|
||||
case InstructionsEnum.itInstructions:
|
||||
return L10n.of(context)!.itInstructionsTitle;
|
||||
|
|
@ -21,10 +37,31 @@ extension InstructionsEnumExtension on InstructionsEnum {
|
|||
return L10n.of(context)!.blurMeansTranslateTitle;
|
||||
case InstructionsEnum.tooltipInstructions:
|
||||
return L10n.of(context)!.tooltipInstructionsTitle;
|
||||
case InstructionsEnum.clickAgainToDeselect:
|
||||
case InstructionsEnum.speechToText:
|
||||
case InstructionsEnum.l1Translation:
|
||||
case InstructionsEnum.translationChoices:
|
||||
ErrorHandler.logError(
|
||||
e: Exception("No title for this instruction"),
|
||||
m: 'InstructionsEnumExtension.title',
|
||||
data: {
|
||||
'this': this,
|
||||
},
|
||||
);
|
||||
debugger(when: kDebugMode);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
String body(BuildContext context) {
|
||||
if (!context.mounted) {
|
||||
ErrorHandler.logError(
|
||||
e: Exception("Context not mounted"),
|
||||
m: 'InstructionsEnumExtension.body for $this',
|
||||
);
|
||||
debugger(when: kDebugMode);
|
||||
return "";
|
||||
}
|
||||
switch (this) {
|
||||
case InstructionsEnum.itInstructions:
|
||||
return L10n.of(context)!.itInstructionsBody;
|
||||
|
|
@ -32,6 +69,14 @@ extension InstructionsEnumExtension on InstructionsEnum {
|
|||
return L10n.of(context)!.clickMessageBody;
|
||||
case InstructionsEnum.blurMeansTranslate:
|
||||
return L10n.of(context)!.blurMeansTranslateBody;
|
||||
case InstructionsEnum.speechToText:
|
||||
return L10n.of(context)!.speechToTextBody;
|
||||
case InstructionsEnum.l1Translation:
|
||||
return L10n.of(context)!.l1TranslationBody;
|
||||
case InstructionsEnum.translationChoices:
|
||||
return L10n.of(context)!.translationChoicesBody;
|
||||
case InstructionsEnum.clickAgainToDeselect:
|
||||
return L10n.of(context)!.clickTheWordAgainToDeselect;
|
||||
case InstructionsEnum.tooltipInstructions:
|
||||
return PlatformInfos.isMobile
|
||||
? L10n.of(context)!.tooltipInstructionsMobileBody
|
||||
|
|
@ -39,7 +84,15 @@ extension InstructionsEnumExtension on InstructionsEnum {
|
|||
}
|
||||
}
|
||||
|
||||
bool get toggledOff {
|
||||
bool toggledOff(BuildContext context) {
|
||||
if (!context.mounted) {
|
||||
ErrorHandler.logError(
|
||||
e: Exception("Context not mounted"),
|
||||
m: 'InstructionsEnumExtension.toggledOff for $this',
|
||||
);
|
||||
debugger(when: kDebugMode);
|
||||
return false;
|
||||
}
|
||||
final instructionSettings =
|
||||
MatrixState.pangeaController.userController.profile.instructionSettings;
|
||||
switch (this) {
|
||||
|
|
@ -51,38 +104,14 @@ extension InstructionsEnumExtension on InstructionsEnum {
|
|||
return instructionSettings.showedBlurMeansTranslate;
|
||||
case InstructionsEnum.tooltipInstructions:
|
||||
return instructionSettings.showedTooltipInstructions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum InlineInstructions {
|
||||
speechToText,
|
||||
l1Translation,
|
||||
translationChoices,
|
||||
}
|
||||
|
||||
extension InlineInstructionsExtension on InlineInstructions {
|
||||
String body(BuildContext context) {
|
||||
switch (this) {
|
||||
case InlineInstructions.speechToText:
|
||||
return L10n.of(context)!.speechToTextBody;
|
||||
case InlineInstructions.l1Translation:
|
||||
return L10n.of(context)!.l1TranslationBody;
|
||||
case InlineInstructions.translationChoices:
|
||||
return L10n.of(context)!.translationChoicesBody;
|
||||
}
|
||||
}
|
||||
|
||||
bool get toggledOff {
|
||||
final instructionSettings =
|
||||
MatrixState.pangeaController.userController.profile.instructionSettings;
|
||||
switch (this) {
|
||||
case InlineInstructions.speechToText:
|
||||
case InstructionsEnum.speechToText:
|
||||
return instructionSettings.showedSpeechToTextTooltip;
|
||||
case InlineInstructions.l1Translation:
|
||||
case InstructionsEnum.l1Translation:
|
||||
return instructionSettings.showedL1TranslationTooltip;
|
||||
case InlineInstructions.translationChoices:
|
||||
case InstructionsEnum.translationChoices:
|
||||
return instructionSettings.showedTranslationChoicesTooltip;
|
||||
case InstructionsEnum.clickAgainToDeselect:
|
||||
return instructionSettings.showedClickAgainToDeselect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,14 @@
|
|||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
enum MessageMode {
|
||||
translation,
|
||||
definition,
|
||||
speechToText,
|
||||
practiceActivity,
|
||||
textToSpeech,
|
||||
practiceActivity
|
||||
definition,
|
||||
translation,
|
||||
speechToText,
|
||||
}
|
||||
|
||||
extension MessageModeExtension on MessageMode {
|
||||
|
|
@ -80,28 +79,39 @@ extension MessageModeExtension on MessageMode {
|
|||
}
|
||||
}
|
||||
|
||||
Color? iconColor(
|
||||
PangeaMessageEvent event,
|
||||
MessageMode? currentMode,
|
||||
bool isUnlocked(
|
||||
int index,
|
||||
int numActivitiesCompleted,
|
||||
bool totallyDone,
|
||||
) =>
|
||||
numActivitiesCompleted >= index || totallyDone;
|
||||
|
||||
Color iconButtonColor(
|
||||
BuildContext context,
|
||||
int index,
|
||||
MessageMode currentMode,
|
||||
int numActivitiesCompleted,
|
||||
bool totallyDone,
|
||||
) {
|
||||
final bool isPracticeActivity = this == MessageMode.practiceActivity;
|
||||
final bool practicing = currentMode == MessageMode.practiceActivity;
|
||||
final bool practiceEnabled = event.hasUncompletedActivity;
|
||||
|
||||
// if this is the practice activity icon, and there's no practice activities available,
|
||||
// and the current mode is not practice, return lower opacity color.
|
||||
if (isPracticeActivity && !practicing && !practiceEnabled) {
|
||||
return Theme.of(context).iconTheme.color?.withOpacity(0.5);
|
||||
//locked
|
||||
if (!isUnlocked(index, numActivitiesCompleted, totallyDone)) {
|
||||
return barAndLockedButtonColor(context);
|
||||
}
|
||||
|
||||
// if this is not a practice activity icon, and practice activities are available,
|
||||
// then return lower opacity color if the current mode is practice.
|
||||
if (!isPracticeActivity && practicing && practiceEnabled) {
|
||||
return Theme.of(context).iconTheme.color?.withOpacity(0.5);
|
||||
//unlocked and active
|
||||
if (this == currentMode) {
|
||||
return Theme.of(context).brightness == Brightness.dark
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.primary;
|
||||
}
|
||||
|
||||
// if this is the current mode, return primary color.
|
||||
return currentMode == this ? Theme.of(context).colorScheme.primary : null;
|
||||
//unlocked and inactive
|
||||
return Theme.of(context).colorScheme.primaryContainer;
|
||||
}
|
||||
|
||||
static Color barAndLockedButtonColor(BuildContext context) {
|
||||
return Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.grey[800]!
|
||||
: Colors.grey[200]!;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ 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';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -349,6 +350,7 @@ class PangeaMessageEvent {
|
|||
_representations?.add(
|
||||
RepresentationEvent(
|
||||
timeline: timeline,
|
||||
parentMessageEvent: _event,
|
||||
content: PangeaRepresentation(
|
||||
langCode: response.langCode,
|
||||
text: response.transcript.text,
|
||||
|
|
@ -362,29 +364,54 @@ class PangeaMessageEvent {
|
|||
return response;
|
||||
}
|
||||
|
||||
PangeaMessageTokens? _tokensSafe(Map<String, dynamic>? content) {
|
||||
try {
|
||||
if (content == null) return null;
|
||||
return PangeaMessageTokens.fromJson(content);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: content,
|
||||
m: "error parsing tokensSent",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ChoreoRecord? get _embeddedChoreo {
|
||||
try {
|
||||
if (_latestEdit.content[ModelKey.choreoRecord] == null) return null;
|
||||
return ChoreoRecord.fromJson(
|
||||
_latestEdit.content[ModelKey.choreoRecord] as Map<String, dynamic>,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: _latestEdit.content,
|
||||
m: "error parsing choreoRecord",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<RepresentationEvent>? _representations;
|
||||
List<RepresentationEvent> get representations {
|
||||
if (_representations != null) return _representations!;
|
||||
_representations = [];
|
||||
|
||||
if (_latestEdit.content[ModelKey.originalSent] != null) {
|
||||
try {
|
||||
final RepresentationEvent sent = RepresentationEvent(
|
||||
parentMessageEvent: _event,
|
||||
content: PangeaRepresentation.fromJson(
|
||||
_latestEdit.content[ModelKey.originalSent] as Map<String, dynamic>,
|
||||
),
|
||||
tokens: _latestEdit.content[ModelKey.tokensSent] != null
|
||||
? PangeaMessageTokens.fromJson(
|
||||
_latestEdit.content[ModelKey.tokensSent]
|
||||
as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
choreo: _latestEdit.content[ModelKey.choreoRecord] != null
|
||||
? ChoreoRecord.fromJson(
|
||||
_latestEdit.content[ModelKey.choreoRecord]
|
||||
as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
tokens: _tokensSafe(
|
||||
_latestEdit.content[ModelKey.tokensSent] as Map<String, dynamic>?,
|
||||
),
|
||||
choreo: _embeddedChoreo,
|
||||
timeline: timeline,
|
||||
);
|
||||
if (_latestEdit.content[ModelKey.choreoRecord] == null) {
|
||||
|
|
@ -413,16 +440,15 @@ class PangeaMessageEvent {
|
|||
try {
|
||||
_representations!.add(
|
||||
RepresentationEvent(
|
||||
parentMessageEvent: _event,
|
||||
content: PangeaRepresentation.fromJson(
|
||||
_latestEdit.content[ModelKey.originalWritten]
|
||||
as Map<String, dynamic>,
|
||||
),
|
||||
tokens: _latestEdit.content[ModelKey.tokensWritten] != null
|
||||
? PangeaMessageTokens.fromJson(
|
||||
_latestEdit.content[ModelKey.tokensWritten]
|
||||
as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
tokens: _tokensSafe(
|
||||
_latestEdit.content[ModelKey.tokensWritten]
|
||||
as Map<String, dynamic>?,
|
||||
),
|
||||
timeline: timeline,
|
||||
),
|
||||
);
|
||||
|
|
@ -442,7 +468,11 @@ class PangeaMessageEvent {
|
|||
PangeaEventTypes.representation,
|
||||
)
|
||||
.map(
|
||||
(e) => RepresentationEvent(event: e, timeline: timeline),
|
||||
(e) => RepresentationEvent(
|
||||
event: e,
|
||||
parentMessageEvent: _event,
|
||||
timeline: timeline,
|
||||
),
|
||||
)
|
||||
.sorted(
|
||||
(a, b) {
|
||||
|
|
@ -487,36 +517,20 @@ class PangeaMessageEvent {
|
|||
final PangeaRepresentation? basis =
|
||||
(originalWritten ?? originalSent)?.content;
|
||||
|
||||
final PangeaRepresentation? pangeaRep =
|
||||
await MatrixState.pangeaController.messageData.getPangeaRepresentation(
|
||||
text: basis?.text ?? _latestEdit.body,
|
||||
source: basis?.langCode,
|
||||
target: langCode,
|
||||
room: _latestEdit.room,
|
||||
);
|
||||
if (pangeaRep == null) return null;
|
||||
// clear representations cache so the new representation event can be added
|
||||
// when next requested
|
||||
_representations = null;
|
||||
|
||||
MatrixState.pangeaController.messageData
|
||||
.sendRepresentationMatrixEvent(
|
||||
representation: pangeaRep,
|
||||
messageEventId: _latestEdit.eventId,
|
||||
room: _latestEdit.room,
|
||||
target: langCode,
|
||||
)
|
||||
.then(
|
||||
(value) {
|
||||
representations.add(
|
||||
RepresentationEvent(
|
||||
event: value,
|
||||
timeline: timeline,
|
||||
),
|
||||
);
|
||||
},
|
||||
).onError(
|
||||
(error, stackTrace) => ErrorHandler.logError(e: error, s: stackTrace),
|
||||
return MatrixState.pangeaController.messageData.getPangeaRepresentation(
|
||||
req: FullTextTranslationRequestModel(
|
||||
text: basis?.text ?? _latestEdit.body,
|
||||
srcLang: basis?.langCode,
|
||||
tgtLang: langCode,
|
||||
userL2: l2Code ?? LanguageKeys.unknownLanguage,
|
||||
userL1: l1Code ?? LanguageKeys.unknownLanguage,
|
||||
),
|
||||
messageEvent: _event,
|
||||
);
|
||||
|
||||
return pangeaRep;
|
||||
}
|
||||
|
||||
RepresentationEvent? get originalSent => representations
|
||||
|
|
@ -556,7 +570,9 @@ class PangeaMessageEvent {
|
|||
|
||||
// this is just showActivityIcon now but will include
|
||||
// logic for showing
|
||||
bool get showMessageButtons => hasUncompletedActivity;
|
||||
// NOTE: turning this off for now
|
||||
bool get showMessageButtons => false;
|
||||
// bool get showMessageButtons => hasUncompletedActivity;
|
||||
|
||||
/// Returns a boolean value indicating whether to show an activity icon for this message event.
|
||||
///
|
||||
|
|
@ -572,9 +588,16 @@ class PangeaMessageEvent {
|
|||
return practiceActivities.any((activity) => !(activity.isComplete));
|
||||
}
|
||||
|
||||
int get numberOfActivitiesCompleted {
|
||||
return practiceActivities.where((activity) => activity.isComplete).length;
|
||||
}
|
||||
|
||||
String? get l2Code =>
|
||||
MatrixState.pangeaController.languageController.activeL2Code();
|
||||
|
||||
String? get l1Code =>
|
||||
MatrixState.pangeaController.languageController.userL1?.langCode;
|
||||
|
||||
String get messageDisplayLangCode {
|
||||
final bool immersionMode = MatrixState
|
||||
.pangeaController.permissionsController
|
||||
|
|
@ -587,6 +610,14 @@ class PangeaMessageEvent {
|
|||
return langCode ?? LanguageKeys.unknownLanguage;
|
||||
}
|
||||
|
||||
/// Gets the message display text for the current language code.
|
||||
/// If the message display text is not available for the current language code,
|
||||
/// it returns the message body.
|
||||
String get messageDisplayText {
|
||||
final String? text = representationByLanguage(messageDisplayLangCode)?.text;
|
||||
return text ?? body;
|
||||
}
|
||||
|
||||
List<PangeaMatch>? errorSteps(String lemma) {
|
||||
final RepresentationEvent? repEvent = originalSent ?? originalWritten;
|
||||
if (repEvent?.choreo == null) return null;
|
||||
|
|
@ -636,6 +667,7 @@ class PangeaMessageEvent {
|
|||
String langCode, {
|
||||
bool debug = false,
|
||||
}) {
|
||||
// @wcjord - disabled try catch for testing
|
||||
try {
|
||||
debugger(when: debug);
|
||||
final List<PracticeActivityEvent> activities = [];
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import '../constants/pangea_event_types.dart';
|
|||
import '../models/choreo_record.dart';
|
||||
import '../models/representation_content_model.dart';
|
||||
import '../utils/error_handler.dart';
|
||||
import 'pangea_tokens_event.dart';
|
||||
|
||||
class RepresentationEvent {
|
||||
Event? _event;
|
||||
|
|
@ -25,9 +24,11 @@ class RepresentationEvent {
|
|||
PangeaMessageTokens? _tokens;
|
||||
ChoreoRecord? _choreo;
|
||||
Timeline timeline;
|
||||
Event parentMessageEvent;
|
||||
|
||||
RepresentationEvent({
|
||||
required this.timeline,
|
||||
required this.parentMessageEvent,
|
||||
Event? event,
|
||||
PangeaRepresentation? content,
|
||||
PangeaMessageTokens? tokens,
|
||||
|
|
@ -97,28 +98,43 @@ class RepresentationEvent {
|
|||
);
|
||||
}
|
||||
|
||||
_tokens = tokenEvents.first.getPangeaContent<PangeaMessageTokens>();
|
||||
final PangeaMessageTokens storedTokens =
|
||||
tokenEvents.first.getPangeaContent<PangeaMessageTokens>();
|
||||
|
||||
if (PangeaToken.reconstructText(storedTokens.tokens) != text) {
|
||||
ErrorHandler.logError(
|
||||
m: 'Stored tokens do not match text for representation',
|
||||
s: StackTrace.current,
|
||||
data: {
|
||||
'text': text,
|
||||
'tokens': storedTokens.tokens,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
_tokens = storedTokens;
|
||||
|
||||
return _tokens?.tokens;
|
||||
}
|
||||
|
||||
Future<List<PangeaToken>?> tokensGlobal(BuildContext context) async {
|
||||
Future<List<PangeaToken>> tokensGlobal(BuildContext context) async {
|
||||
if (tokens != null) return tokens!;
|
||||
|
||||
if (_event == null) {
|
||||
// debugger(when: kDebugMode);
|
||||
// ErrorHandler.logError(
|
||||
// m: '_event and _tokens both null',
|
||||
// s: StackTrace.current,
|
||||
// );
|
||||
return null;
|
||||
ErrorHandler.logError(
|
||||
m: 'representation with no _event and no tokens got tokens directly. This means an original_sent with no tokens. This should not happen in messages sent after September 25',
|
||||
s: StackTrace.current,
|
||||
data: {
|
||||
'content': content.toJson(),
|
||||
'event': _event?.toJson(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final Event? tokensEvent =
|
||||
await MatrixState.pangeaController.messageData.getTokenEvent(
|
||||
context: context,
|
||||
repEventId: _event!.eventId,
|
||||
room: _event!.room,
|
||||
final List<PangeaToken> res =
|
||||
await MatrixState.pangeaController.messageData.getTokens(
|
||||
repEventId: _event?.eventId,
|
||||
room: _event?.room ?? parentMessageEvent.room,
|
||||
// Jordan - for just tokens, it's not clear which languages to pass
|
||||
req: TokensRequestModel(
|
||||
fullText: text,
|
||||
|
|
@ -129,11 +145,7 @@ class RepresentationEvent {
|
|||
),
|
||||
);
|
||||
|
||||
if (tokensEvent == null) return null;
|
||||
|
||||
_tokens = TokensEvent(event: tokensEvent).tokens;
|
||||
|
||||
return _tokens?.tokens;
|
||||
return res;
|
||||
}
|
||||
|
||||
ChoreoRecord? get choreo {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:developer';
|
|||
|
||||
import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -35,8 +36,14 @@ class PracticeActivityEvent {
|
|||
}
|
||||
|
||||
PracticeActivityModel get practiceActivity {
|
||||
_content ??= event.getPangeaContent<PracticeActivityModel>();
|
||||
return _content!;
|
||||
try {
|
||||
_content ??= event.getPangeaContent<PracticeActivityModel>();
|
||||
return _content!;
|
||||
} catch (e, s) {
|
||||
final contentMap = event.content;
|
||||
debugger(when: kDebugMode);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// All completion records assosiated with this activity
|
||||
|
|
@ -56,24 +63,34 @@ class PracticeActivityEvent {
|
|||
|
||||
/// 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;
|
||||
List<PracticeActivityRecordEvent> get allUserRecords => allRecords
|
||||
.where(
|
||||
(recordEvent) =>
|
||||
recordEvent.event.senderId == recordEvent.event.room.client.userID,
|
||||
)
|
||||
.toList();
|
||||
|
||||
/// Get the most recent user record for this activity
|
||||
PracticeActivityRecordEvent? get latestUserRecord {
|
||||
final List<PracticeActivityRecordEvent> userRecords = allUserRecords;
|
||||
if (userRecords.isEmpty) return null;
|
||||
return userRecords.reduce(
|
||||
(a, b) => a.event.originServerTs.isAfter(b.event.originServerTs) ? a : b,
|
||||
);
|
||||
}
|
||||
|
||||
DateTime? get lastCompletedAt => latestUserRecord?.event.originServerTs;
|
||||
|
||||
String get parentMessageId => event.relationshipEventId!;
|
||||
|
||||
/// Checks if there are any user records in the list for this activity,
|
||||
/// and, if so, then the activity is complete
|
||||
bool get isComplete => userRecord != null;
|
||||
bool get isComplete => latestUserRecord != null;
|
||||
|
||||
ExistingActivityMetaData get activityRequestMetaData =>
|
||||
ExistingActivityMetaData(
|
||||
activityEventId: event.eventId,
|
||||
tgtConstructs: practiceActivity.tgtConstructs,
|
||||
activityType: practiceActivity.activityType,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,53 +6,76 @@ import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
|||
/// the process of filtering / sorting / displaying the events.
|
||||
/// Takes a construct type and a list of events
|
||||
class ConstructListModel {
|
||||
final ConstructTypeEnum type;
|
||||
final ConstructTypeEnum? type;
|
||||
final List<OneConstructUse> _uses;
|
||||
List<ConstructUses>? _constructList;
|
||||
List<ConstructUseTypeUses>? _typedConstructs;
|
||||
|
||||
/// A map of lemmas to ConstructUses, each of which contains a lemma
|
||||
/// key = lemmma + constructType.string, value = ConstructUses
|
||||
Map<String, ConstructUses>? _constructMap;
|
||||
|
||||
ConstructListModel({
|
||||
required this.type,
|
||||
uses,
|
||||
}) : _uses = uses ?? [];
|
||||
|
||||
List<ConstructUses>? _constructs;
|
||||
List<ConstructUseTypeUses>? _typedConstructs;
|
||||
required List<OneConstructUse> uses,
|
||||
}) : _uses = uses;
|
||||
|
||||
List<OneConstructUse> get uses =>
|
||||
_uses.where((use) => use.constructType == type).toList();
|
||||
_uses.where((use) => use.constructType == type || type == null).toList();
|
||||
|
||||
/// All unique lemmas used in the construct events
|
||||
List<String> get lemmas => constructs.map((e) => e.lemma).toSet().toList();
|
||||
List<String> get lemmas => constructList.map((e) => e.lemma).toSet().toList();
|
||||
|
||||
/// A list of ConstructUses, each of which contains a lemma and
|
||||
/// a list of uses, sorted by the number of uses
|
||||
List<ConstructUses> get constructs {
|
||||
// the list of uses doesn't change so we don't have to re-calculate this
|
||||
if (_constructs != null) return _constructs!;
|
||||
/// A map of lemmas to ConstructUses, each of which contains a lemma
|
||||
/// key = lemmma + constructType.string, value = ConstructUses
|
||||
void _buildConstructMap() {
|
||||
final Map<String, List<OneConstructUse>> lemmaToUses = {};
|
||||
for (final use in uses) {
|
||||
if (use.lemma == null) continue;
|
||||
lemmaToUses[use.lemma!] ??= [];
|
||||
lemmaToUses[use.lemma!]!.add(use);
|
||||
lemmaToUses[use.lemma! + use.constructType.string] ??= [];
|
||||
lemmaToUses[use.lemma! + use.constructType.string]!.add(use);
|
||||
}
|
||||
|
||||
final constructUses = lemmaToUses.entries
|
||||
.map(
|
||||
(entry) => ConstructUses(
|
||||
lemma: entry.key,
|
||||
uses: entry.value,
|
||||
constructType: type,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
_constructMap = lemmaToUses.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
ConstructUses(
|
||||
uses: value,
|
||||
constructType: value.first.constructType,
|
||||
lemma: value.first.lemma!,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
constructUses.sort((a, b) {
|
||||
ConstructUses? getConstructUses(String lemma, ConstructTypeEnum type) {
|
||||
if (_constructMap == null) _buildConstructMap();
|
||||
return _constructMap![lemma + type.string];
|
||||
}
|
||||
|
||||
/// A list of ConstructUses, each of which contains a lemma and
|
||||
/// a list of uses, sorted by the number of uses
|
||||
List<ConstructUses> get constructList {
|
||||
// the list of uses doesn't change so we don't have to re-calculate this
|
||||
if (_constructList != null) return _constructList!;
|
||||
|
||||
if (_constructMap == null) _buildConstructMap();
|
||||
|
||||
_constructList = _constructMap!.values.toList();
|
||||
|
||||
_constructList!.sort((a, b) {
|
||||
final comp = b.uses.length.compareTo(a.uses.length);
|
||||
if (comp != 0) return comp;
|
||||
return a.lemma.compareTo(b.lemma);
|
||||
});
|
||||
|
||||
_constructs = constructUses;
|
||||
return constructUses;
|
||||
return _constructList!;
|
||||
}
|
||||
|
||||
get maxXPPerLemma {
|
||||
return type != null
|
||||
? type!.maxXPPerLemma
|
||||
: ConstructTypeEnum.vocab.maxXPPerLemma;
|
||||
}
|
||||
|
||||
/// A list of ConstructUseTypeUses, each of which
|
||||
|
|
@ -60,7 +83,7 @@ class ConstructListModel {
|
|||
List<ConstructUseTypeUses> get typedConstructs {
|
||||
if (_typedConstructs != null) return _typedConstructs!;
|
||||
final List<ConstructUseTypeUses> typedConstructs = [];
|
||||
for (final construct in constructs) {
|
||||
for (final construct in constructList) {
|
||||
final typeToUses = <ConstructUseTypeEnum, List<OneConstructUse>>{};
|
||||
for (final use in construct.uses) {
|
||||
typeToUses[use.useType] ??= [];
|
||||
|
|
@ -70,7 +93,7 @@ class ConstructListModel {
|
|||
typedConstructs.add(
|
||||
ConstructUseTypeUses(
|
||||
lemma: construct.lemma,
|
||||
constructType: type,
|
||||
constructType: typeEntry.value.first.constructType,
|
||||
useType: typeEntry.key,
|
||||
uses: typeEntry.value,
|
||||
),
|
||||
|
|
@ -125,6 +148,16 @@ class ConstructUses {
|
|||
(total, use) => total + use.useType.pointValue,
|
||||
);
|
||||
}
|
||||
|
||||
DateTime? _lastUsed;
|
||||
DateTime? get lastUsed {
|
||||
if (_lastUsed != null) return _lastUsed;
|
||||
final lastUse = uses.fold<DateTime?>(null, (DateTime? last, use) {
|
||||
if (last == null) return use.timeStamp;
|
||||
return use.timeStamp.isAfter(last) ? use.timeStamp : last;
|
||||
});
|
||||
return _lastUsed = lastUse;
|
||||
}
|
||||
}
|
||||
|
||||
/// One lemma, a use type, and a list of uses
|
||||
|
|
|
|||
|
|
@ -76,8 +76,12 @@ class OneConstructUse {
|
|||
String? lemma;
|
||||
String? form;
|
||||
List<String> categories;
|
||||
ConstructTypeEnum? constructType;
|
||||
ConstructTypeEnum constructType;
|
||||
ConstructUseTypeEnum useType;
|
||||
|
||||
/// Used to unqiuely identify the construct use. Useful in the case
|
||||
/// that a users makes the same type of mistake multiple times in a
|
||||
/// message, and those uses need to be disinguished.
|
||||
String? id;
|
||||
ConstructUseMetaData metadata;
|
||||
|
||||
|
|
@ -96,6 +100,11 @@ class OneConstructUse {
|
|||
DateTime get timeStamp => metadata.timeStamp;
|
||||
|
||||
factory OneConstructUse.fromJson(Map<String, dynamic> json) {
|
||||
final constructType = json['constructType'] != null
|
||||
? ConstructTypeUtil.fromString(json['constructType'])
|
||||
: null;
|
||||
debugger(when: kDebugMode && constructType == null);
|
||||
|
||||
return OneConstructUse(
|
||||
useType: ConstructUseTypeEnum.values
|
||||
.firstWhereOrNull((e) => e.string == json['useType']) ??
|
||||
|
|
@ -105,9 +114,7 @@ class OneConstructUse {
|
|||
categories: json['categories'] != null
|
||||
? List<String>.from(json['categories'])
|
||||
: [],
|
||||
constructType: json['constructType'] != null
|
||||
? ConstructTypeUtil.fromString(json['constructType'])
|
||||
: null,
|
||||
constructType: constructType ?? ConstructTypeEnum.vocab,
|
||||
id: json['id'],
|
||||
metadata: ConstructUseMetaData(
|
||||
eventId: json['msgId'],
|
||||
|
|
@ -117,7 +124,7 @@ class OneConstructUse {
|
|||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson([bool condensed = false]) {
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {
|
||||
'useType': useType.string,
|
||||
'chatId': metadata.roomId,
|
||||
|
|
@ -125,10 +132,10 @@ class OneConstructUse {
|
|||
'form': form,
|
||||
'msgId': metadata.eventId,
|
||||
};
|
||||
if (!condensed && lemma != null) data['lemma'] = lemma!;
|
||||
if (!condensed && constructType != null) {
|
||||
data['constructType'] = constructType!.string;
|
||||
}
|
||||
|
||||
data['lemma'] = lemma!;
|
||||
data['constructType'] = constructType.string;
|
||||
|
||||
if (id != null) data['id'] = id;
|
||||
data['categories'] = categories;
|
||||
return data;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/bot_mode.dart';
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.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 BotOptionsModel {
|
||||
int? languageLevel;
|
||||
String topic;
|
||||
|
|
@ -30,7 +30,7 @@ class BotOptionsModel {
|
|||
this.topic = "General Conversation",
|
||||
this.keywords = const [],
|
||||
this.safetyModeration = true,
|
||||
this.mode = "discussion",
|
||||
this.mode = BotMode.discussion,
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Discussion Mode Options
|
||||
|
|
@ -62,7 +62,7 @@ class BotOptionsModel {
|
|||
? json[ModelKey.languageLevel]
|
||||
: null,
|
||||
safetyModeration: json[ModelKey.safetyModeration] ?? true,
|
||||
mode: json[ModelKey.mode] ?? "discussion",
|
||||
mode: json[ModelKey.mode] ?? BotMode.discussion,
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Discussion Mode Options
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ 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';
|
||||
import 'package:fluffychat/pangea/models/span_data.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -116,6 +117,7 @@ class IGCTextData {
|
|||
) async {
|
||||
//should be already added to choreoRecord
|
||||
//TODO - that should be done in the same function to avoid error potential
|
||||
|
||||
final PangeaMatch pangeaMatch = matches[matchIndex];
|
||||
|
||||
if (pangeaMatch.match.choices == null) {
|
||||
|
|
@ -126,14 +128,53 @@ class IGCTextData {
|
|||
return;
|
||||
}
|
||||
|
||||
final String replacement = pangeaMatch.match.choices![choiceIndex].value;
|
||||
final SpanChoice replacement = pangeaMatch.match.choices![choiceIndex];
|
||||
|
||||
originalInput = originalInput.replaceRange(
|
||||
pangeaMatch.match.offset,
|
||||
pangeaMatch.match.offset + pangeaMatch.match.length,
|
||||
replacement,
|
||||
replacement.value,
|
||||
);
|
||||
|
||||
// replace the tokens that are part of the match
|
||||
// with the tokens in the replacement
|
||||
// start is inclusive
|
||||
final startIndex = tokenIndexByOffset(pangeaMatch.match.offset);
|
||||
// end is exclusive, hence the +1
|
||||
final endIndex = tokenIndexByOffset(
|
||||
pangeaMatch.match.offset + pangeaMatch.match.length,
|
||||
) +
|
||||
1;
|
||||
|
||||
// for all tokens after the replacement, update their offsets
|
||||
for (int i = endIndex; i < tokens.length; i++) {
|
||||
tokens[i].text.offset +=
|
||||
replacement.value.length - pangeaMatch.match.length;
|
||||
}
|
||||
|
||||
// clone the list for debugging purposes
|
||||
final List<PangeaToken> newTokens = List.from(tokens);
|
||||
|
||||
// replace the tokens in the list
|
||||
newTokens.replaceRange(startIndex, endIndex, replacement.tokens);
|
||||
|
||||
final String newFullText = PangeaToken.reconstructText(newTokens);
|
||||
|
||||
if (newFullText != originalInput && kDebugMode) {
|
||||
PangeaToken.reconstructText(newTokens, debugWalkThrough: true);
|
||||
ErrorHandler.logError(
|
||||
m: "reconstructed text not working",
|
||||
s: StackTrace.current,
|
||||
data: {
|
||||
"originalInput": originalInput,
|
||||
"newFullText": newFullText,
|
||||
"match": pangeaMatch.match.toJson(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
tokens = newTokens;
|
||||
|
||||
//update offsets in existing matches to reflect the change
|
||||
//Question - remove matches that overlap with the accepted one?
|
||||
// see case of "quiero ver un fix"
|
||||
|
|
@ -142,18 +183,10 @@ class IGCTextData {
|
|||
for (final match in matches) {
|
||||
match.match.fullText = originalInput;
|
||||
if (match.match.offset > pangeaMatch.match.offset) {
|
||||
match.match.offset += replacement.length - pangeaMatch.match.length;
|
||||
match.match.offset +=
|
||||
replacement.value.length - pangeaMatch.match.length;
|
||||
}
|
||||
}
|
||||
//quiero ver un fix
|
||||
//match offset zero and length of full text or 16
|
||||
//fix is repplaced by arreglo and now the length needs to be 20
|
||||
//if the accepted span is within another span, then the length of that span needs
|
||||
//needs to be increased by the difference between the new and old length
|
||||
//if the two spans are overlapping, what happens?
|
||||
//------
|
||||
// ----- -> ---
|
||||
//if there is any overlap, maybe igc needs to run again?
|
||||
}
|
||||
|
||||
void removeMatchByOffset(int offset) {
|
||||
|
|
@ -163,9 +196,8 @@ class IGCTextData {
|
|||
}
|
||||
}
|
||||
|
||||
int tokenIndexByOffset(cursorOffset) => tokens.indexWhere(
|
||||
(token) =>
|
||||
token.text.offset <= cursorOffset && cursorOffset <= token.end,
|
||||
int tokenIndexByOffset(int cursorOffset) => tokens.indexWhere(
|
||||
(token) => token.start <= cursorOffset && cursorOffset <= token.end,
|
||||
);
|
||||
|
||||
List<int> matchIndicesByOffset(int offset) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../constants/model_keys.dart';
|
||||
|
|
@ -24,6 +28,47 @@ class PangeaToken {
|
|||
required this.morph,
|
||||
});
|
||||
|
||||
/// reconstructs the text from the tokens
|
||||
/// [tokens] - the tokens to reconstruct
|
||||
/// [debugWalkThrough] - if true, will start the debugger
|
||||
static String reconstructText(
|
||||
List<PangeaToken> tokens, {
|
||||
bool debugWalkThrough = false,
|
||||
int startTokenIndex = 0,
|
||||
int endTokenIndex = -1,
|
||||
}) {
|
||||
debugger(when: kDebugMode && debugWalkThrough);
|
||||
|
||||
if (endTokenIndex == -1) {
|
||||
endTokenIndex = tokens.length;
|
||||
}
|
||||
|
||||
final List<PangeaToken> subset =
|
||||
tokens.sublist(startTokenIndex, endTokenIndex);
|
||||
|
||||
if (subset.isEmpty) {
|
||||
debugger(when: kDebugMode);
|
||||
return '';
|
||||
}
|
||||
|
||||
if (subset.length == 1) {
|
||||
return subset.first.text.content;
|
||||
}
|
||||
|
||||
String reconstruction = "";
|
||||
for (int i = 0; i < subset.length; i++) {
|
||||
int whitespace = subset[i].text.offset -
|
||||
(i > 0 ? (subset[i - 1].text.offset + subset[i - 1].text.length) : 0);
|
||||
|
||||
if (whitespace < 0) {
|
||||
whitespace = 0;
|
||||
}
|
||||
reconstruction += ' ' * whitespace + subset[i].text.content;
|
||||
}
|
||||
|
||||
return reconstruction;
|
||||
}
|
||||
|
||||
static Lemma _getLemmas(String text, dynamic json) {
|
||||
if (json != null) {
|
||||
// July 24, 2024 - we're changing from a list to a single lemma and this is for backwards compatibility
|
||||
|
|
@ -67,7 +112,45 @@ class PangeaToken {
|
|||
'morph': morph,
|
||||
};
|
||||
|
||||
/// alias for the offset
|
||||
int get start => text.offset;
|
||||
|
||||
/// alias for the end of the token ie offset + length
|
||||
int get end => text.offset + text.length;
|
||||
|
||||
/// create an empty tokenWithXP object
|
||||
TokenWithXP get emptyTokenWithXP {
|
||||
final List<ConstructWithXP> constructs = [];
|
||||
|
||||
constructs.add(
|
||||
ConstructWithXP(
|
||||
id: ConstructIdentifier(
|
||||
lemma: lemma.text,
|
||||
type: ConstructTypeEnum.vocab,
|
||||
),
|
||||
xp: 0,
|
||||
lastUsed: null,
|
||||
),
|
||||
);
|
||||
|
||||
for (final morph in morph.entries) {
|
||||
constructs.add(
|
||||
ConstructWithXP(
|
||||
id: ConstructIdentifier(
|
||||
lemma: morph.key,
|
||||
type: ConstructTypeEnum.morph,
|
||||
),
|
||||
xp: 0,
|
||||
lastUsed: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return TokenWithXP(
|
||||
token: this,
|
||||
constructs: constructs,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PangeaTokenText {
|
||||
|
|
@ -96,4 +179,18 @@ class PangeaTokenText {
|
|||
|
||||
Map<String, dynamic> toJson() =>
|
||||
{_offsetKey: offset, _contentKey: content, _lengthKey: length};
|
||||
|
||||
//override equals and hashcode
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is PangeaTokenText) {
|
||||
return other.offset == offset &&
|
||||
other.content == content &&
|
||||
other.length == length;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => offset.hashCode ^ content.hashCode ^ length.hashCode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,267 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
|
||||
class ConstructWithXP {
|
||||
final ConstructIdentifier id;
|
||||
int xp;
|
||||
DateTime? lastUsed;
|
||||
|
||||
ConstructWithXP({
|
||||
required this.id,
|
||||
required this.xp,
|
||||
required this.lastUsed,
|
||||
});
|
||||
|
||||
factory ConstructWithXP.fromJson(Map<String, dynamic> json) {
|
||||
return ConstructWithXP(
|
||||
id: ConstructIdentifier.fromJson(
|
||||
json['construct_id'] as Map<String, dynamic>,
|
||||
),
|
||||
xp: json['xp'] as int,
|
||||
lastUsed: json['last_used'] != null
|
||||
? DateTime.parse(json['last_used'] as String)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = {
|
||||
'construct_id': id.toJson(),
|
||||
'xp': xp,
|
||||
'last_used': lastUsed?.toIso8601String(),
|
||||
};
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
class TokenWithXP {
|
||||
final PangeaToken token;
|
||||
final List<ConstructWithXP> constructs;
|
||||
|
||||
DateTime? get lastUsed {
|
||||
return constructs.fold<DateTime?>(
|
||||
null,
|
||||
(previousValue, element) {
|
||||
if (previousValue == null) return element.lastUsed;
|
||||
if (element.lastUsed == null) return previousValue;
|
||||
return element.lastUsed!.isAfter(previousValue)
|
||||
? element.lastUsed
|
||||
: previousValue;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
int get xp {
|
||||
return constructs.fold<int>(
|
||||
0,
|
||||
(previousValue, element) => previousValue + element.xp,
|
||||
);
|
||||
}
|
||||
|
||||
TokenWithXP({
|
||||
required this.token,
|
||||
required this.constructs,
|
||||
});
|
||||
|
||||
factory TokenWithXP.fromJson(Map<String, dynamic> json) {
|
||||
return TokenWithXP(
|
||||
token: PangeaToken.fromJson(json['token'] as Map<String, dynamic>),
|
||||
constructs: (json['constructs'] as List)
|
||||
.map((e) => ConstructWithXP.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'token': token.toJson(),
|
||||
'constructs_with_xp': constructs.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is TokenWithXP &&
|
||||
other.token.text == token.text &&
|
||||
other.lastUsed == lastUsed;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return token.text.hashCode ^ lastUsed.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
class ExistingActivityMetaData {
|
||||
final String activityEventId;
|
||||
final List<ConstructIdentifier> tgtConstructs;
|
||||
final ActivityTypeEnum activityType;
|
||||
|
||||
ExistingActivityMetaData({
|
||||
required this.activityEventId,
|
||||
required this.tgtConstructs,
|
||||
required this.activityType,
|
||||
});
|
||||
|
||||
factory ExistingActivityMetaData.fromJson(Map<String, dynamic> json) {
|
||||
return ExistingActivityMetaData(
|
||||
activityEventId: json['activity_event_id'] as String,
|
||||
tgtConstructs: ((json['tgt_constructs'] ?? json['target_constructs'])
|
||||
as List)
|
||||
.map((e) => ConstructIdentifier.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
activityType: ActivityTypeEnum.values.firstWhere(
|
||||
(element) =>
|
||||
element.string == json['activity_type'] as String ||
|
||||
element.string.split('.').last == json['activity_type'] as String,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'activity_event_id': activityEventId,
|
||||
'target_constructs': tgtConstructs.map((e) => e.toJson()).toList(),
|
||||
'activity_type': activityType.string,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// includes feedback text and the bad activity model
|
||||
class ActivityQualityFeedback {
|
||||
final String feedbackText;
|
||||
final PracticeActivityModel badActivity;
|
||||
|
||||
ActivityQualityFeedback({
|
||||
required this.feedbackText,
|
||||
required this.badActivity,
|
||||
});
|
||||
|
||||
factory ActivityQualityFeedback.fromJson(Map<String, dynamic> json) {
|
||||
return ActivityQualityFeedback(
|
||||
feedbackText: json['feedback_text'] as String,
|
||||
badActivity: PracticeActivityModel.fromJson(
|
||||
json['bad_activity'] as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'feedback_text': feedbackText,
|
||||
'bad_activity': badActivity.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MessageActivityRequest {
|
||||
final String userL1;
|
||||
final String userL2;
|
||||
|
||||
final String messageText;
|
||||
|
||||
final ActivityQualityFeedback? activityQualityFeedback;
|
||||
|
||||
/// tokens with their associated constructs and xp
|
||||
final List<TokenWithXP> tokensWithXP;
|
||||
|
||||
/// make the server aware of existing activities for potential reuse
|
||||
final List<ExistingActivityMetaData> existingActivities;
|
||||
|
||||
final String messageId;
|
||||
|
||||
MessageActivityRequest({
|
||||
required this.userL1,
|
||||
required this.userL2,
|
||||
required this.messageText,
|
||||
required this.tokensWithXP,
|
||||
required this.messageId,
|
||||
required this.existingActivities,
|
||||
required this.activityQualityFeedback,
|
||||
});
|
||||
|
||||
factory MessageActivityRequest.fromJson(Map<String, dynamic> json) {
|
||||
return MessageActivityRequest(
|
||||
userL1: json['user_l1'] as String,
|
||||
userL2: json['user_l2'] as String,
|
||||
messageText: json['message_text'] as String,
|
||||
tokensWithXP: (json['tokens_with_xp'] as List)
|
||||
.map((e) => TokenWithXP.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
messageId: json['message_id'] as String,
|
||||
existingActivities: (json['existing_activities'] as List)
|
||||
.map(
|
||||
(e) => ExistingActivityMetaData.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
activityQualityFeedback: json['activity_quality_feedback'] != null
|
||||
? ActivityQualityFeedback.fromJson(
|
||||
json['activity_quality_feedback'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'user_l1': userL1,
|
||||
'user_l2': userL2,
|
||||
'message_text': messageText,
|
||||
'tokens_with_xp': tokensWithXP.map((e) => e.toJson()).toList(),
|
||||
'message_id': messageId,
|
||||
'existing_activities': existingActivities.map((e) => e.toJson()).toList(),
|
||||
'activity_quality_feedback': activityQualityFeedback?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
// equals accounts for message_id and last_used of each token
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is MessageActivityRequest &&
|
||||
other.messageId == messageId &&
|
||||
const ListEquality().equals(other.tokensWithXP, tokensWithXP);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return messageId.hashCode ^ const ListEquality().hash(tokensWithXP);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageActivityResponse {
|
||||
final PracticeActivityModel? activity;
|
||||
final bool finished;
|
||||
final String? existingActivityEventId;
|
||||
|
||||
MessageActivityResponse({
|
||||
required this.activity,
|
||||
required this.finished,
|
||||
required this.existingActivityEventId,
|
||||
});
|
||||
|
||||
factory MessageActivityResponse.fromJson(Map<String, dynamic> json) {
|
||||
return MessageActivityResponse(
|
||||
activity: json['activity'] != null
|
||||
? PracticeActivityModel.fromJson(
|
||||
json['activity'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
finished: json['finished'] as bool,
|
||||
existingActivityEventId: json['existing_activity_event_id'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'activity': activity?.toJson(),
|
||||
'finished': finished,
|
||||
'existing_activity_event_id': existingActivityEventId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MultipleChoice {
|
||||
|
|
@ -12,10 +15,18 @@ class MultipleChoice {
|
|||
required this.question,
|
||||
required this.choices,
|
||||
required this.answer,
|
||||
this.spanDisplayDetails,
|
||||
required this.spanDisplayDetails,
|
||||
});
|
||||
|
||||
bool isCorrect(int index) => index == correctAnswerIndex;
|
||||
/// we've had some bugs where the index is not expected
|
||||
/// so we're going to check if the index or the value is correct
|
||||
/// and if not, we'll investigate
|
||||
bool isCorrect(String value, int index) {
|
||||
if (value != choices[index]) {
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
return value == answer || index == correctAnswerIndex;
|
||||
}
|
||||
|
||||
bool get isValidQuestion => choices.contains(answer);
|
||||
|
||||
|
|
@ -27,13 +38,15 @@ class MultipleChoice {
|
|||
index == correctAnswerIndex ? AppConfig.success : AppConfig.warning;
|
||||
|
||||
factory MultipleChoice.fromJson(Map<String, dynamic> json) {
|
||||
final spanDisplay = json['span_display_details'] != null &&
|
||||
json['span_display_details'] is Map
|
||||
? RelevantSpanDisplayDetails.fromJson(json['span_display_details'])
|
||||
: null;
|
||||
return MultipleChoice(
|
||||
question: json['question'] as String,
|
||||
choices: (json['choices'] as List).map((e) => e as String).toList(),
|
||||
answer: json['answer'] ?? json['correct_answer'] as String,
|
||||
spanDisplayDetails: json['span_display_details'] != null
|
||||
? RelevantSpanDisplayDetails.fromJson(json['span_display_details'])
|
||||
: null,
|
||||
spanDisplayDetails: spanDisplay,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +55,7 @@ class MultipleChoice {
|
|||
'question': question,
|
||||
'choices': choices,
|
||||
'answer': answer,
|
||||
'span_display_details': spanDisplayDetails,
|
||||
'span_display_details': spanDisplayDetails?.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,21 @@ class ConstructIdentifier {
|
|||
'type': type.string,
|
||||
};
|
||||
}
|
||||
|
||||
// override operator == and hashCode
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is ConstructIdentifier &&
|
||||
other.lemma == lemma &&
|
||||
other.type == type;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return lemma.hashCode ^ type.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
class CandidateMessage {
|
||||
|
|
@ -238,9 +253,25 @@ class PracticeActivityModel {
|
|||
this.freeResponse,
|
||||
});
|
||||
|
||||
String get question {
|
||||
switch (activityType) {
|
||||
case ActivityTypeEnum.multipleChoice:
|
||||
return multipleChoice!.question;
|
||||
case ActivityTypeEnum.listening:
|
||||
return listening!.text;
|
||||
case ActivityTypeEnum.speaking:
|
||||
return speaking!.text;
|
||||
case ActivityTypeEnum.freeResponse:
|
||||
return freeResponse!.question;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
factory PracticeActivityModel.fromJson(Map<String, dynamic> json) {
|
||||
return PracticeActivityModel(
|
||||
tgtConstructs: (json['tgt_constructs'] as List)
|
||||
tgtConstructs: ((json['tgt_constructs'] ?? json['target_constructs'])
|
||||
as List)
|
||||
.map((e) => ConstructIdentifier.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
langCode: json['lang_code'] as String,
|
||||
|
|
@ -248,7 +279,9 @@ class PracticeActivityModel {
|
|||
activityType: json['activity_type'] == "multipleChoice"
|
||||
? ActivityTypeEnum.multipleChoice
|
||||
: ActivityTypeEnum.values.firstWhere(
|
||||
(e) => e.string == json['activity_type'],
|
||||
(e) =>
|
||||
e.string == json['activity_type'] as String ||
|
||||
e.string.split('.').last == json['activity_type'] as String,
|
||||
),
|
||||
multipleChoice: json['multiple_choice'] != null
|
||||
? MultipleChoice.fromJson(
|
||||
|
|
@ -269,12 +302,15 @@ class PracticeActivityModel {
|
|||
);
|
||||
}
|
||||
|
||||
RelevantSpanDisplayDetails? get relevantSpanDisplayDetails =>
|
||||
multipleChoice?.spanDisplayDetails;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'tgt_constructs': tgtConstructs.map((e) => e.toJson()).toList(),
|
||||
'target_constructs': tgtConstructs.map((e) => e.toJson()).toList(),
|
||||
'lang_code': langCode,
|
||||
'msg_id': msgId,
|
||||
'activity_type': activityType.toString().split('.').last,
|
||||
'activity_type': activityType.string,
|
||||
'multiple_choice': multipleChoice?.toJson(),
|
||||
'listening': listening?.toJson(),
|
||||
'speaking': speaking?.toJson(),
|
||||
|
|
@ -282,20 +318,32 @@ class PracticeActivityModel {
|
|||
};
|
||||
}
|
||||
|
||||
RelevantSpanDisplayDetails? getRelevantSpanDisplayDetails() {
|
||||
switch (activityType) {
|
||||
case ActivityTypeEnum.multipleChoice:
|
||||
return multipleChoice?.spanDisplayDetails;
|
||||
case ActivityTypeEnum.listening:
|
||||
return null;
|
||||
case ActivityTypeEnum.speaking:
|
||||
return null;
|
||||
case ActivityTypeEnum.freeResponse:
|
||||
return null;
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
return null;
|
||||
}
|
||||
// override operator == and hashCode
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is PracticeActivityModel &&
|
||||
const ListEquality().equals(other.tgtConstructs, tgtConstructs) &&
|
||||
other.langCode == langCode &&
|
||||
other.msgId == msgId &&
|
||||
other.activityType == activityType &&
|
||||
other.multipleChoice == multipleChoice &&
|
||||
other.listening == listening &&
|
||||
other.speaking == speaking &&
|
||||
other.freeResponse == freeResponse;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return const ListEquality().hash(tgtConstructs) ^
|
||||
langCode.hashCode ^
|
||||
msgId.hashCode ^
|
||||
activityType.hashCode ^
|
||||
multipleChoice.hashCode ^
|
||||
listening.hashCode ^
|
||||
speaking.hashCode ^
|
||||
freeResponse.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -332,7 +380,23 @@ class RelevantSpanDisplayDetails {
|
|||
return {
|
||||
'offset': offset,
|
||||
'length': length,
|
||||
'display_instructions': displayInstructions,
|
||||
'display_instructions': displayInstructions.string,
|
||||
};
|
||||
}
|
||||
|
||||
// override operator == and hashCode
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is RelevantSpanDisplayDetails &&
|
||||
other.offset == offset &&
|
||||
other.length == length &&
|
||||
other.displayInstructions == displayInstructions;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return offset.hashCode ^ length.hashCode ^ displayInstructions.hashCode;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,9 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/construct_use_type_enum.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/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class PracticeActivityRecordModel {
|
||||
final String? question;
|
||||
|
|
@ -57,11 +54,9 @@ class PracticeActivityRecordModel {
|
|||
return responses[responses.length - 1];
|
||||
}
|
||||
|
||||
ConstructUseTypeEnum get useType => latestResponse?.score != null
|
||||
? (latestResponse!.score > 0
|
||||
? ConstructUseTypeEnum.corPA
|
||||
: ConstructUseTypeEnum.incPA)
|
||||
: ConstructUseTypeEnum.unk;
|
||||
bool hasTextResponse(String text) {
|
||||
return responses.any((element) => element.text == text);
|
||||
}
|
||||
|
||||
void addResponse({
|
||||
String? text,
|
||||
|
|
@ -80,59 +75,26 @@ class PracticeActivityRecordModel {
|
|||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugger();
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a list of [OneConstructUse] objects representing the uses of the practice activity.
|
||||
///
|
||||
/// The [practiceActivity] parameter is the parent event, representing the activity itself.
|
||||
/// The [event] parameter is the record event, if available.
|
||||
/// The [metadata] parameter is the metadata for the construct use, used if the record event isn't available.
|
||||
///
|
||||
/// If [event] and [metadata] are both null, an empty list is returned.
|
||||
///
|
||||
/// The method iterates over the [tgtConstructs] of the [practiceActivity] and creates a [OneConstructUse] object for each construct.
|
||||
List<OneConstructUse> uses(
|
||||
PracticeActivityEvent practiceActivity, {
|
||||
Event? event,
|
||||
ConstructUseMetaData? metadata,
|
||||
}) {
|
||||
try {
|
||||
if (event == null && metadata == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
}
|
||||
|
||||
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: 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,
|
||||
metadata: ConstructUseMetaData(
|
||||
roomId: event?.roomId ?? metadata!.roomId,
|
||||
eventId: practiceActivity.parentMessageId,
|
||||
timeStamp: event?.originServerTs ?? metadata!.timeStamp,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return uses;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(e: e, s: s, data: event?.toJson());
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
/// The method iterates over the [responses] to get [OneConstructUse] objects for each
|
||||
List<OneConstructUse> usesForAllResponses(
|
||||
PracticeActivityModel practiceActivity,
|
||||
ConstructUseMetaData metadata,
|
||||
) =>
|
||||
responses
|
||||
.toSet()
|
||||
.expand(
|
||||
(response) => response.toUses(practiceActivity, metadata),
|
||||
)
|
||||
.toList();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
|
|
@ -168,6 +130,26 @@ class ActivityRecordResponse {
|
|||
required this.timestamp,
|
||||
});
|
||||
|
||||
//TODO - differentiate into different activity types
|
||||
ConstructUseTypeEnum get useType =>
|
||||
score > 0 ? ConstructUseTypeEnum.corPA : ConstructUseTypeEnum.incPA;
|
||||
|
||||
// for each target construct create a OneConstructUse object
|
||||
List<OneConstructUse> toUses(
|
||||
PracticeActivityModel practiceActivity,
|
||||
ConstructUseMetaData metadata,
|
||||
) =>
|
||||
practiceActivity.tgtConstructs
|
||||
.map(
|
||||
(construct) => OneConstructUse(
|
||||
lemma: construct.lemma,
|
||||
constructType: construct.type,
|
||||
useType: useType,
|
||||
metadata: metadata,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
factory ActivityRecordResponse.fromJson(Map<String, dynamic> json) {
|
||||
return ActivityRecordResponse(
|
||||
text: json['text'] as String?,
|
||||
|
|
|
|||
|
|
@ -14,8 +14,17 @@ class PangeaMessageTokens {
|
|||
});
|
||||
|
||||
factory PangeaMessageTokens.fromJson(Map<String, dynamic> json) {
|
||||
// "tokens" was accidentally used as the key in the first implementation
|
||||
// _tokensKey is the correct key
|
||||
final something = json[_tokensKey] ?? json["tokens"];
|
||||
|
||||
final Iterable tokensIterable = something is Iterable
|
||||
? something
|
||||
: something is String
|
||||
? jsonDecode(json[_tokensKey])
|
||||
: null;
|
||||
return PangeaMessageTokens(
|
||||
tokens: (jsonDecode(json[_tokensKey] ?? "[]") as Iterable)
|
||||
tokens: tokensIterable
|
||||
.map((e) => PangeaToken.fromJson(e))
|
||||
.toList()
|
||||
.cast<PangeaToken>(),
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ class UserInstructions {
|
|||
bool showedSpeechToTextTooltip;
|
||||
bool showedL1TranslationTooltip;
|
||||
bool showedTranslationChoicesTooltip;
|
||||
bool showedClickAgainToDeselect;
|
||||
|
||||
UserInstructions({
|
||||
this.showedItInstructions = false,
|
||||
|
|
@ -198,12 +199,12 @@ class UserInstructions {
|
|||
this.showedSpeechToTextTooltip = false,
|
||||
this.showedL1TranslationTooltip = false,
|
||||
this.showedTranslationChoicesTooltip = false,
|
||||
this.showedClickAgainToDeselect = false,
|
||||
});
|
||||
|
||||
factory UserInstructions.fromJson(Map<String, dynamic> json) =>
|
||||
UserInstructions(
|
||||
showedItInstructions:
|
||||
json[InstructionsEnum.itInstructions.toString()] ?? false,
|
||||
showedItInstructions: json[InstructionsEnum.itInstructions.toString()],
|
||||
showedClickMessage:
|
||||
json[InstructionsEnum.clickMessage.toString()] ?? false,
|
||||
showedBlurMeansTranslate:
|
||||
|
|
@ -211,11 +212,13 @@ class UserInstructions {
|
|||
showedTooltipInstructions:
|
||||
json[InstructionsEnum.tooltipInstructions.toString()] ?? false,
|
||||
showedL1TranslationTooltip:
|
||||
json[InlineInstructions.l1Translation.toString()] ?? false,
|
||||
json[InstructionsEnum.l1Translation.toString()] ?? false,
|
||||
showedTranslationChoicesTooltip:
|
||||
json[InlineInstructions.translationChoices.toString()] ?? false,
|
||||
json[InstructionsEnum.translationChoices.toString()] ?? false,
|
||||
showedSpeechToTextTooltip:
|
||||
json[InlineInstructions.speechToText.toString()] ?? false,
|
||||
json[InstructionsEnum.speechToText.toString()] ?? false,
|
||||
showedClickAgainToDeselect:
|
||||
json[InstructionsEnum.clickAgainToDeselect.toString()] ?? false,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -226,12 +229,13 @@ class UserInstructions {
|
|||
showedBlurMeansTranslate;
|
||||
data[InstructionsEnum.tooltipInstructions.toString()] =
|
||||
showedTooltipInstructions;
|
||||
data[InlineInstructions.l1Translation.toString()] =
|
||||
data[InstructionsEnum.l1Translation.toString()] =
|
||||
showedL1TranslationTooltip;
|
||||
data[InlineInstructions.translationChoices.toString()] =
|
||||
data[InstructionsEnum.translationChoices.toString()] =
|
||||
showedTranslationChoicesTooltip;
|
||||
data[InlineInstructions.speechToText.toString()] =
|
||||
showedSpeechToTextTooltip;
|
||||
data[InstructionsEnum.speechToText.toString()] = showedSpeechToTextTooltip;
|
||||
data[InstructionsEnum.clickAgainToDeselect.toString()] =
|
||||
showedClickAgainToDeselect;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
|
@ -258,20 +262,25 @@ class UserInstructions {
|
|||
as bool?) ??
|
||||
false,
|
||||
showedL1TranslationTooltip:
|
||||
(accountData[InlineInstructions.l1Translation.toString()]
|
||||
?.content[InlineInstructions.l1Translation.toString()]
|
||||
(accountData[InstructionsEnum.l1Translation.toString()]
|
||||
?.content[InstructionsEnum.l1Translation.toString()]
|
||||
as bool?) ??
|
||||
false,
|
||||
showedTranslationChoicesTooltip: (accountData[
|
||||
InlineInstructions.translationChoices.toString()]
|
||||
?.content[InlineInstructions.translationChoices.toString()]
|
||||
showedTranslationChoicesTooltip:
|
||||
(accountData[InstructionsEnum.translationChoices.toString()]
|
||||
?.content[InstructionsEnum.translationChoices.toString()]
|
||||
as bool?) ??
|
||||
false,
|
||||
showedSpeechToTextTooltip:
|
||||
(accountData[InstructionsEnum.speechToText.toString()]
|
||||
?.content[InstructionsEnum.speechToText.toString()]
|
||||
as bool?) ??
|
||||
false,
|
||||
showedClickAgainToDeselect: (accountData[
|
||||
InstructionsEnum.clickAgainToDeselect.toString()]
|
||||
?.content[InstructionsEnum.clickAgainToDeselect.toString()]
|
||||
as bool?) ??
|
||||
false,
|
||||
showedSpeechToTextTooltip:
|
||||
(accountData[InlineInstructions.speechToText.toString()]
|
||||
?.content[InlineInstructions.speechToText.toString()]
|
||||
as bool?) ??
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ class PApiUrls {
|
|||
static String textToSpeech = "${Environment.choreoApi}/text_to_speech";
|
||||
static String speechToText = "${Environment.choreoApi}/speech_to_text";
|
||||
|
||||
static String messageActivityGeneration =
|
||||
"${Environment.choreoApi}/practice/message";
|
||||
|
||||
///-------------------------------- revenue cat --------------------------
|
||||
static String rcApiV1 = "https://api.revenuecat.com/v1";
|
||||
static String rcApiV2 =
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
|
||||
setState(() => fetchingUses = true);
|
||||
try {
|
||||
final List<OneConstructUse> uses = constructs?.constructs
|
||||
final List<OneConstructUse> uses = constructs?.constructList
|
||||
.firstWhereOrNull(
|
||||
(element) => element.lemma == currentLemma,
|
||||
)
|
||||
|
|
@ -276,7 +276,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
);
|
||||
}
|
||||
|
||||
if (constructs?.constructs.isEmpty ?? true) {
|
||||
if (constructs?.constructList.isEmpty ?? true) {
|
||||
return Expanded(
|
||||
child: Center(child: Text(L10n.of(context)!.noDataFound)),
|
||||
);
|
||||
|
|
@ -284,17 +284,17 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
|
||||
return Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: constructs!.constructs.length,
|
||||
itemCount: constructs!.constructList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
constructs!.constructs[index].lemma,
|
||||
constructs!.constructList[index].lemma,
|
||||
),
|
||||
subtitle: Text(
|
||||
'${L10n.of(context)!.total} ${constructs!.constructs[index].uses.length}',
|
||||
'${L10n.of(context)!.total} ${constructs!.constructList[index].uses.length}',
|
||||
),
|
||||
onTap: () async {
|
||||
final String lemma = constructs!.constructs[index].lemma;
|
||||
final String lemma = constructs!.constructList[index].lemma;
|
||||
setCurrentLemma(lemma);
|
||||
fetchUses().then((_) => showConstructMessagesDialog());
|
||||
},
|
||||
|
|
@ -320,7 +320,8 @@ class ConstructMessagesDialog extends StatelessWidget {
|
|||
|
||||
final msgEventMatches = controller.getMessageEventMatches();
|
||||
|
||||
final currentConstruct = controller.constructs!.constructs.firstWhereOrNull(
|
||||
final currentConstruct =
|
||||
controller.constructs!.constructList.firstWhereOrNull(
|
||||
(construct) => construct.lemma == controller.currentLemma,
|
||||
);
|
||||
final noData = currentConstruct == null ||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
import '../config/environment.dart';
|
||||
import '../models/pangea_token_model.dart';
|
||||
import '../network/requests.dart';
|
||||
import '../network/urls.dart';
|
||||
|
||||
class ContextualizationTranslationRepo {
|
||||
//Question for Jordan - is this for an individual token or could it be a span?
|
||||
static Future<ContextTranslationResponseModel> translate({
|
||||
required String accessToken,
|
||||
required ContextualTranslationRequestModel request,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//Question for Jordan - is this for an individual token or could it be a span?
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
|
|
@ -10,10 +11,58 @@ import '../network/requests.dart';
|
|||
import '../network/urls.dart';
|
||||
|
||||
class FullTextTranslationRepo {
|
||||
static final Map<String, FullTextTranslationResponseModel> _cache = {};
|
||||
static Timer? _cacheTimer;
|
||||
|
||||
// start a timer to clear the cache
|
||||
static void startCacheTimer() {
|
||||
_cacheTimer = Timer.periodic(const Duration(minutes: 3), (timer) {
|
||||
clearCache();
|
||||
});
|
||||
}
|
||||
|
||||
// stop the cache time (optional)
|
||||
static void stopCacheTimer() {
|
||||
_cacheTimer?.cancel();
|
||||
}
|
||||
|
||||
// method to clear the cache
|
||||
static void clearCache() {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
static String _generateCacheKey({
|
||||
required String text,
|
||||
required String srcLang,
|
||||
required String tgtLang,
|
||||
required int offset,
|
||||
required int length,
|
||||
bool? deepL,
|
||||
}) {
|
||||
return '${text.hashCode}-$srcLang-$tgtLang-$deepL-$offset-$length';
|
||||
}
|
||||
|
||||
static Future<FullTextTranslationResponseModel> translate({
|
||||
required String accessToken,
|
||||
required FullTextTranslationRequestModel request,
|
||||
}) async {
|
||||
// start cache timer when the first API call is made
|
||||
startCacheTimer();
|
||||
|
||||
final cacheKey = _generateCacheKey(
|
||||
text: request.text,
|
||||
srcLang: request.srcLang ?? '',
|
||||
tgtLang: request.tgtLang,
|
||||
offset: request.offset ?? 0,
|
||||
length: request.length ?? 0,
|
||||
deepL: request.deepL,
|
||||
);
|
||||
|
||||
// check cache first
|
||||
if (_cache.containsKey(cacheKey)) {
|
||||
return _cache[cacheKey]!;
|
||||
}
|
||||
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
accessToken: accessToken,
|
||||
|
|
@ -24,9 +73,14 @@ class FullTextTranslationRepo {
|
|||
body: request.toJson(),
|
||||
);
|
||||
|
||||
return FullTextTranslationResponseModel.fromJson(
|
||||
final responseModel = FullTextTranslationResponseModel.fromJson(
|
||||
jsonDecode(utf8.decode(res.bodyBytes)),
|
||||
);
|
||||
|
||||
// store response in cache
|
||||
_cache[cacheKey] = responseModel;
|
||||
|
||||
return responseModel;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +117,33 @@ class FullTextTranslationRequestModel {
|
|||
ModelKey.offset: offset,
|
||||
ModelKey.length: length,
|
||||
};
|
||||
|
||||
// override equals and hashcode
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is FullTextTranslationRequestModel &&
|
||||
other.text == text &&
|
||||
other.srcLang == srcLang &&
|
||||
other.tgtLang == tgtLang &&
|
||||
other.userL2 == userL2 &&
|
||||
other.userL1 == userL1 &&
|
||||
other.deepL == deepL &&
|
||||
other.offset == offset &&
|
||||
other.length == length;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
text.hashCode ^
|
||||
srcLang.hashCode ^
|
||||
tgtLang.hashCode ^
|
||||
userL2.hashCode ^
|
||||
userL1.hashCode ^
|
||||
deepL.hashCode ^
|
||||
offset.hashCode ^
|
||||
length.hashCode;
|
||||
}
|
||||
|
||||
class FullTextTranslationResponseModel {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,20 @@ class TokensRequestModel {
|
|||
ModelKey.userL1: userL1,
|
||||
ModelKey.userL2: userL2,
|
||||
};
|
||||
|
||||
// override equals and hashcode
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is TokensRequestModel &&
|
||||
other.fullText == fullText &&
|
||||
other.userL1 == userL1 &&
|
||||
other.userL2 == userL2;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => fullText.hashCode ^ userL1.hashCode ^ userL2.hashCode;
|
||||
}
|
||||
|
||||
class TokensResponseModel {
|
||||
|
|
|
|||
|
|
@ -1,60 +1,71 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class InlineTooltip extends StatelessWidget {
|
||||
final String body;
|
||||
final InstructionsEnum instructionsEnum;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const InlineTooltip({
|
||||
super.key,
|
||||
required this.body,
|
||||
required this.instructionsEnum,
|
||||
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,
|
||||
),
|
||||
),
|
||||
if (instructionsEnum.toggledOff(context)) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
color: Theme.of(context).colorScheme.primary.withAlpha(20),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
const WidgetSpan(
|
||||
child: Icon(
|
||||
Icons.lightbulb,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const WidgetSpan(
|
||||
child: SizedBox(width: 5),
|
||||
),
|
||||
TextSpan(
|
||||
text: body,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Lightbulb icon on the left
|
||||
Icon(
|
||||
Icons.lightbulb,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Text in the middle
|
||||
Expanded(
|
||||
child: Text(
|
||||
instructionsEnum.body(context),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1.5,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Close button on the right
|
||||
IconButton(
|
||||
constraints: const BoxConstraints(),
|
||||
icon: Icon(
|
||||
Icons.close_outlined,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
onPressed: () {
|
||||
MatrixState.pangeaController.instructions.setToggledOff(
|
||||
instructionsEnum,
|
||||
true,
|
||||
);
|
||||
onClose();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import 'package:collection/collection.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';
|
||||
|
||||
|
|
@ -24,54 +22,40 @@ class InstructionsController {
|
|||
/// Instruction popup has already been shown this session
|
||||
final Map<String, bool> _instructionsShown = {};
|
||||
|
||||
/// Returns true if the user requested this popup not be shown again
|
||||
bool? toggledOff(String key) {
|
||||
final bool? instruction = InstructionsEnum.values
|
||||
.firstWhereOrNull((value) => value.toString() == key)
|
||||
?.toggledOff;
|
||||
final bool? tooltip = InlineInstructions.values
|
||||
.firstWhereOrNull((value) => value.toString() == key)
|
||||
?.toggledOff;
|
||||
return instruction ?? tooltip;
|
||||
}
|
||||
|
||||
InstructionsController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
}
|
||||
|
||||
/// Returns true if the instructions were closed
|
||||
/// or turned off by the user via the toggle switch
|
||||
bool wereInstructionsTurnedOff(String key) {
|
||||
return toggledOff(key) ?? _instructionsClosed[key] ?? false;
|
||||
}
|
||||
|
||||
void turnOffInstruction(String key) => _instructionsClosed[key] = true;
|
||||
|
||||
void updateEnableInstructions(
|
||||
String key,
|
||||
void setToggledOff(
|
||||
InstructionsEnum key,
|
||||
bool value,
|
||||
) {
|
||||
_pangeaController.userController.updateProfile((profile) {
|
||||
if (key == InstructionsEnum.itInstructions.toString()) {
|
||||
profile.instructionSettings.showedItInstructions = value;
|
||||
}
|
||||
if (key == InstructionsEnum.clickMessage.toString()) {
|
||||
profile.instructionSettings.showedClickMessage = value;
|
||||
}
|
||||
if (key == InstructionsEnum.blurMeansTranslate.toString()) {
|
||||
profile.instructionSettings.showedBlurMeansTranslate = value;
|
||||
}
|
||||
if (key == InstructionsEnum.tooltipInstructions.toString()) {
|
||||
profile.instructionSettings.showedTooltipInstructions = value;
|
||||
}
|
||||
if (key == InlineInstructions.speechToText.toString()) {
|
||||
profile.instructionSettings.showedSpeechToTextTooltip = value;
|
||||
}
|
||||
if (key == InlineInstructions.l1Translation.toString()) {
|
||||
profile.instructionSettings.showedL1TranslationTooltip = value;
|
||||
}
|
||||
if (key == InlineInstructions.translationChoices.toString()) {
|
||||
profile.instructionSettings.showedTranslationChoicesTooltip = value;
|
||||
switch (key) {
|
||||
case InstructionsEnum.speechToText:
|
||||
profile.instructionSettings.showedSpeechToTextTooltip = value;
|
||||
break;
|
||||
case InstructionsEnum.l1Translation:
|
||||
profile.instructionSettings.showedL1TranslationTooltip = value;
|
||||
break;
|
||||
case InstructionsEnum.translationChoices:
|
||||
profile.instructionSettings.showedTranslationChoicesTooltip = value;
|
||||
break;
|
||||
case InstructionsEnum.tooltipInstructions:
|
||||
profile.instructionSettings.showedTooltipInstructions = value;
|
||||
break;
|
||||
case InstructionsEnum.itInstructions:
|
||||
profile.instructionSettings.showedItInstructions = value;
|
||||
break;
|
||||
case InstructionsEnum.clickMessage:
|
||||
profile.instructionSettings.showedClickMessage = value;
|
||||
break;
|
||||
case InstructionsEnum.blurMeansTranslate:
|
||||
profile.instructionSettings.showedBlurMeansTranslate = value;
|
||||
break;
|
||||
case InstructionsEnum.clickAgainToDeselect:
|
||||
profile.instructionSettings.showedClickAgainToDeselect = value;
|
||||
break;
|
||||
}
|
||||
return profile;
|
||||
});
|
||||
|
|
@ -90,7 +74,7 @@ class InstructionsController {
|
|||
}
|
||||
_instructionsShown[key.toString()] = true;
|
||||
|
||||
if (wereInstructionsTurnedOff(key.toString())) {
|
||||
if (key.toggledOff(context)) {
|
||||
return;
|
||||
}
|
||||
if (L10n.of(context) == null) {
|
||||
|
|
@ -142,31 +126,6 @@ class InstructionsController {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a widget that will be added to existing widget
|
||||
/// which displays hint text defined in the enum extension
|
||||
Widget getInstructionInlineTooltip(
|
||||
BuildContext context,
|
||||
InlineInstructions key,
|
||||
VoidCallback onClose,
|
||||
) {
|
||||
if (wereInstructionsTurnedOff(key.toString())) {
|
||||
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: InlineInstructions.speechToText.body(context),
|
||||
onClose: onClose,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// User can toggle on to prevent Instruction Card
|
||||
|
|
@ -196,12 +155,10 @@ class InstructionsToggleState extends State<InstructionsToggle> {
|
|||
return SwitchListTile.adaptive(
|
||||
activeColor: AppConfig.activeToggleColor,
|
||||
title: Text(L10n.of(context)!.doNotShowAgain),
|
||||
value: pangeaController.instructions.wereInstructionsTurnedOff(
|
||||
widget.instructionsKey.toString(),
|
||||
),
|
||||
value: widget.instructionsKey.toggledOff(context),
|
||||
onChanged: ((value) async {
|
||||
pangeaController.instructions.updateEnableInstructions(
|
||||
widget.instructionsKey.toString(),
|
||||
pangeaController.instructions.setToggledOff(
|
||||
widget.instructionsKey,
|
||||
value,
|
||||
);
|
||||
setState(() {});
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ void pLogoutAction(BuildContext context, {bool? isDestructiveAction}) async {
|
|||
final matrix = Matrix.of(context);
|
||||
|
||||
// before wiping out locally cached construct data, save it to the server
|
||||
await MatrixState.pangeaController.myAnalytics.updateAnalytics();
|
||||
await MatrixState.pangeaController.myAnalytics
|
||||
.sendLocalAnalyticsToAnalyticsRoom();
|
||||
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ class MatchCopy {
|
|||
}
|
||||
final String afterColon = splits.join();
|
||||
|
||||
print("grammar rule ${match.match.rule!.id}");
|
||||
debugPrint("grammar rule ${match.match.rule!.id}");
|
||||
|
||||
switch (afterColon) {
|
||||
case MatchRuleIds.interactiveTranslation:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/constants/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/progress_bar/animated_level_dart.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/progress_bar/progress_bar_details.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -21,17 +20,11 @@ class LevelBar extends StatefulWidget {
|
|||
class LevelBarState extends State<LevelBar> {
|
||||
double prevWidth = 0;
|
||||
|
||||
double get width {
|
||||
const perLevel = AnalyticsConstants.xpPerLevel;
|
||||
final percent = (widget.details.currentPoints % perLevel) / perLevel;
|
||||
return widget.progressBarDetails.totalWidth * percent;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant LevelBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.details.currentPoints != widget.details.currentPoints) {
|
||||
setState(() => prevWidth = width);
|
||||
setState(() => prevWidth = widget.details.width);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +33,7 @@ class LevelBarState extends State<LevelBar> {
|
|||
return AnimatedLevelBar(
|
||||
height: widget.progressBarDetails.height,
|
||||
beginWidth: prevWidth,
|
||||
endWidth: width,
|
||||
endWidth: widget.details.width,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import 'dart:ui';
|
|||
class LevelBarDetails {
|
||||
final Color fillColor;
|
||||
final int currentPoints;
|
||||
final double width;
|
||||
|
||||
const LevelBarDetails({
|
||||
required this.fillColor,
|
||||
required this.currentPoints,
|
||||
required this.width,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
60
lib/pangea/widgets/chat/chat_view_background.dart
Normal file
60
lib/pangea/widgets/chat/chat_view_background.dart
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChatViewBackground extends StatefulWidget {
|
||||
final Choreographer choreographer;
|
||||
const ChatViewBackground({
|
||||
super.key,
|
||||
required this.choreographer,
|
||||
});
|
||||
|
||||
@override
|
||||
ChatViewBackgroundState createState() => ChatViewBackgroundState();
|
||||
}
|
||||
|
||||
class ChatViewBackgroundState extends State<ChatViewBackground> {
|
||||
StreamSubscription? _choreoSub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Rebuild the widget each time there's an update from choreo
|
||||
_choreoSub = widget.choreographer.stateListener.stream.listen((_) {
|
||||
setState(() {});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_choreoSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.choreographer.itController.willOpen
|
||||
? Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Material(
|
||||
borderOnForeground: false,
|
||||
color: const Color.fromRGBO(0, 0, 0, 1).withAlpha(150),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 2.5, sigmaY: 2.5),
|
||||
child: Container(
|
||||
height: double.infinity,
|
||||
width: double.infinity,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:fluffychat/pages/chat/events/audio_player.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -9,10 +11,12 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
class MessageAudioCard extends StatefulWidget {
|
||||
final PangeaMessageEvent messageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
|
||||
const MessageAudioCard({
|
||||
super.key,
|
||||
required this.messageEvent,
|
||||
required this.overlayController,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -45,7 +49,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
audioFile =
|
||||
await widget.messageEvent.getMatrixAudioFile(langCode, context);
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
} catch (e, _) {
|
||||
} catch (e, s) {
|
||||
debugPrint(StackTrace.current.toString());
|
||||
if (!mounted) return;
|
||||
setState(() => _isLoading = false);
|
||||
|
|
@ -56,7 +60,7 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
);
|
||||
ErrorHandler.logError(
|
||||
e: Exception(),
|
||||
s: StackTrace.current,
|
||||
s: s,
|
||||
m: 'something wrong getting audio in MessageAudioCardState',
|
||||
data: {
|
||||
'widget.messageEvent.messageDisplayLangCode':
|
||||
|
|
@ -70,29 +74,33 @@ class MessageAudioCardState extends State<MessageAudioCard> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
//once we have audio for words, we'll play that
|
||||
if (widget.overlayController.isSelection) {
|
||||
widget.overlayController.clearSelection();
|
||||
}
|
||||
|
||||
fetchAudio();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(minHeight: minCardHeight),
|
||||
alignment: Alignment.center,
|
||||
child: _isLoading
|
||||
? const ToolbarContentLoadingIndicator()
|
||||
: localAudioEvent != null || audioFile != null
|
||||
? Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 250,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
AudioPlayerWidget(
|
||||
localAudioEvent,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
matrixFile: audioFile,
|
||||
autoplay: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
? Column(
|
||||
children: [
|
||||
AudioPlayerWidget(
|
||||
localAudioEvent,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
matrixFile: audioFile,
|
||||
autoplay: true,
|
||||
),
|
||||
],
|
||||
)
|
||||
: const CardErrorWidget(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,48 +1,66 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/message.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar_buttons.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_footer.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_header.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_message.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class MessageSelectionOverlay extends StatefulWidget {
|
||||
final ChatController controller;
|
||||
final Event event;
|
||||
final Event? nextEvent;
|
||||
final Event? prevEvent;
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final MessageMode? initialMode;
|
||||
final MessageTextSelection textSelection;
|
||||
final ChatController chatController;
|
||||
late final Event _event;
|
||||
late final Event? _nextEvent;
|
||||
late final Event? _prevEvent;
|
||||
late final PangeaMessageEvent _pangeaMessageEvent;
|
||||
|
||||
const MessageSelectionOverlay({
|
||||
required this.controller,
|
||||
required this.event,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.textSelection,
|
||||
this.initialMode,
|
||||
this.nextEvent,
|
||||
this.prevEvent,
|
||||
MessageSelectionOverlay({
|
||||
required this.chatController,
|
||||
required Event event,
|
||||
required PangeaMessageEvent pangeaMessageEvent,
|
||||
required Event? nextEvent,
|
||||
required Event? prevEvent,
|
||||
super.key,
|
||||
});
|
||||
}) {
|
||||
_pangeaMessageEvent = pangeaMessageEvent;
|
||||
_nextEvent = nextEvent;
|
||||
_prevEvent = prevEvent;
|
||||
_event = event;
|
||||
}
|
||||
|
||||
@override
|
||||
MessageSelectionOverlayState createState() => MessageSelectionOverlayState();
|
||||
MessageOverlayController createState() => MessageOverlayController();
|
||||
}
|
||||
|
||||
class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
||||
class MessageOverlayController extends State<MessageSelectionOverlay>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
Animation<double>? _overlayPositionAnimation;
|
||||
|
||||
MessageMode toolbarMode = MessageMode.translation;
|
||||
PangeaTokenText? _selectedSpan;
|
||||
|
||||
/// The number of activities that need to be completed before the toolbar is unlocked
|
||||
/// If we don't have any good activities for them, we'll decrease this number
|
||||
static const int neededActivities = 3;
|
||||
|
||||
int activitiesLeftToComplete = neededActivities;
|
||||
|
||||
PangeaMessageEvent get pangeaMessageEvent => widget._pangeaMessageEvent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -50,8 +68,155 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
vsync: this,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
);
|
||||
|
||||
activitiesLeftToComplete = activitiesLeftToComplete -
|
||||
widget._pangeaMessageEvent.numberOfActivitiesCompleted;
|
||||
|
||||
setInitialToolbarMode();
|
||||
}
|
||||
|
||||
/// We need to check if the setState call is safe to call immediately
|
||||
/// Kept getting the error: setState() or markNeedsBuild() called during build.
|
||||
/// This is a workaround to prevent that error
|
||||
@override
|
||||
void setState(VoidCallback fn) {
|
||||
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle ||
|
||||
SchedulerBinding.instance.schedulerPhase ==
|
||||
SchedulerPhase.postFrameCallbacks) {
|
||||
// It's safe to call setState immediately
|
||||
super.setState(fn);
|
||||
} else {
|
||||
// Defer the setState call to after the current frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
super.setState(fn);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool get isPracticeComplete => activitiesLeftToComplete <= 0;
|
||||
|
||||
/// When an activity is completed, we need to update the state
|
||||
/// and check if the toolbar should be unlocked
|
||||
void onActivityFinish() {
|
||||
if (!mounted) return;
|
||||
activitiesLeftToComplete -= 1;
|
||||
clearSelection();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
/// In some cases, we need to exit the practice flow and let the user
|
||||
/// interact with the toolbar without completing activities
|
||||
void exitPracticeFlow() {
|
||||
clearSelection();
|
||||
activitiesLeftToComplete = 0;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> setInitialToolbarMode() async {
|
||||
if (widget._pangeaMessageEvent.isAudioMessage) {
|
||||
toolbarMode = MessageMode.speechToText;
|
||||
return;
|
||||
}
|
||||
|
||||
if (activitiesLeftToComplete > 0) {
|
||||
toolbarMode = MessageMode.practiceActivity;
|
||||
return;
|
||||
}
|
||||
|
||||
if (MatrixState.pangeaController.userController.profile.userSettings
|
||||
.autoPlayMessages) {
|
||||
toolbarMode = MessageMode.textToSpeech;
|
||||
return;
|
||||
}
|
||||
|
||||
toolbarMode = MessageMode.translation;
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
updateToolbarMode(MessageMode mode) {
|
||||
setState(() {
|
||||
toolbarMode = mode;
|
||||
});
|
||||
}
|
||||
|
||||
/// The text that the toolbar should target
|
||||
/// If there is no selectedSpan, then the whole message is the target
|
||||
/// If there is a selectedSpan, then the target is the selected text
|
||||
String get targetText {
|
||||
if (_selectedSpan == null) {
|
||||
return widget._pangeaMessageEvent.messageDisplayText;
|
||||
}
|
||||
|
||||
return widget._pangeaMessageEvent.messageDisplayText.substring(
|
||||
_selectedSpan!.offset,
|
||||
_selectedSpan!.offset + _selectedSpan!.length,
|
||||
);
|
||||
}
|
||||
|
||||
void onClickOverlayMessageToken(
|
||||
PangeaToken token,
|
||||
) {
|
||||
if ([MessageMode.practiceActivity, MessageMode.textToSpeech]
|
||||
.contains(toolbarMode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if there's no selected span, then select the token
|
||||
if (_selectedSpan == null) {
|
||||
_selectedSpan = token.text;
|
||||
} else {
|
||||
// if there is a selected span, then deselect the token if it's the same
|
||||
if (isTokenSelected(token)) {
|
||||
_selectedSpan = null;
|
||||
} else {
|
||||
// if there is a selected span but it is not the same, then select the token
|
||||
_selectedSpan = token.text;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void clearSelection() {
|
||||
_selectedSpan = null;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void setSelectedSpan(PracticeActivityModel activity) {
|
||||
final RelevantSpanDisplayDetails? span =
|
||||
activity.multipleChoice?.spanDisplayDetails;
|
||||
|
||||
if (span == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedSpan = PangeaTokenText(
|
||||
offset: span.offset,
|
||||
length: span.length,
|
||||
content: widget._pangeaMessageEvent.messageDisplayText
|
||||
.substring(span.offset, span.offset + span.length),
|
||||
);
|
||||
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
/// Whether the given token is currently selected
|
||||
bool isTokenSelected(PangeaToken token) {
|
||||
return _selectedSpan?.offset == token.text.offset &&
|
||||
_selectedSpan?.length == token.text.length;
|
||||
}
|
||||
|
||||
/// Whether the overlay is currently displaying a selection
|
||||
bool get isSelection => _selectedSpan != null;
|
||||
|
||||
PangeaTokenText? get selectedSpan => _selectedSpan;
|
||||
|
||||
final int toolbarButtonsHeight = 50;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
|
@ -62,11 +227,13 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
// position the overlay directly over the underlying message
|
||||
final headerBottomOffset = screenHeight - headerHeight;
|
||||
final footerBottomOffset = footerHeight;
|
||||
final currentBottomOffset =
|
||||
screenHeight - messageOffset!.dy - messageSize!.height;
|
||||
final currentBottomOffset = screenHeight -
|
||||
messageOffset!.dy -
|
||||
messageSize!.height -
|
||||
toolbarButtonsHeight;
|
||||
|
||||
final bool hasHeaderOverflow =
|
||||
messageOffset!.dy < (AppConfig.toolbarMaxHeight + headerHeight);
|
||||
final bool hasHeaderOverflow = (messageOffset!.dy - toolbarButtonsHeight) <
|
||||
(AppConfig.toolbarMaxHeight + headerHeight);
|
||||
final bool hasFooterOverflow = footerHeight > currentBottomOffset;
|
||||
|
||||
if (!hasHeaderOverflow && !hasFooterOverflow) return;
|
||||
|
|
@ -79,7 +246,8 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
// if the overlay would have a footer overflow for this message,
|
||||
// check if shifting the overlay up could cause a header overflow
|
||||
final bottomOffsetDifference = footerHeight - currentBottomOffset;
|
||||
final newTopOffset = messageOffset!.dy - bottomOffsetDifference;
|
||||
final newTopOffset =
|
||||
messageOffset!.dy - bottomOffsetDifference - toolbarButtonsHeight;
|
||||
final bool upshiftCausesHeaderOverflow = hasFooterOverflow &&
|
||||
newTopOffset < (headerHeight + AppConfig.toolbarMaxHeight);
|
||||
|
||||
|
|
@ -108,8 +276,8 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
),
|
||||
);
|
||||
|
||||
widget.controller.scrollController.animateTo(
|
||||
widget.controller.scrollController.offset - scrollOffset,
|
||||
widget.chatController.scrollController.animateTo(
|
||||
widget.chatController.scrollController.offset - scrollOffset,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
);
|
||||
|
|
@ -123,7 +291,7 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
}
|
||||
|
||||
RenderBox? get messageRenderBox => MatrixState.pAnyState.getRenderBox(
|
||||
widget.event.eventId,
|
||||
widget._event.eventId,
|
||||
);
|
||||
|
||||
Size? get messageSize => messageRenderBox?.size;
|
||||
|
|
@ -139,6 +307,8 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
|
||||
double get screenHeight => MediaQuery.of(context).size.height;
|
||||
|
||||
double get screenWidth => MediaQuery.of(context).size.width;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool showDetails = (Matrix.of(context)
|
||||
|
|
@ -146,9 +316,27 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
.getBool(SettingKeys.displayChatDetailsColumn) ??
|
||||
false) &&
|
||||
FluffyThemes.isThreeColumnMode(context) &&
|
||||
widget.controller.room.membership == Membership.join;
|
||||
widget.chatController.room.membership == Membership.join;
|
||||
|
||||
final overlayMessage = ConstrainedBox(
|
||||
// the default spacing between the side of the screen and the message bubble
|
||||
final double messageMargin =
|
||||
pangeaMessageEvent.ownMessage ? Avatar.defaultSize + 16 : 8;
|
||||
|
||||
// the actual spacing between the side of the screen and
|
||||
// the message bubble, accounts for wide screen
|
||||
double extraChatSpace = FluffyThemes.isColumnMode(context)
|
||||
? ((screenWidth -
|
||||
(FluffyThemes.columnWidth * 3.5) -
|
||||
FluffyThemes.navRailWidth) /
|
||||
2) +
|
||||
messageMargin
|
||||
: messageMargin;
|
||||
|
||||
if (extraChatSpace < messageMargin) {
|
||||
extraChatSpace = messageMargin;
|
||||
}
|
||||
|
||||
final overlayMessage = Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: FluffyThemes.columnWidth * 2.5,
|
||||
),
|
||||
|
|
@ -156,77 +344,75 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: widget._pangeaMessageEvent.ownMessage
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: widget.pangeaMessageEvent.ownMessage
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: widget.pangeaMessageEvent.ownMessage
|
||||
? 0
|
||||
: Avatar.defaultSize + 16,
|
||||
right: widget.pangeaMessageEvent.ownMessage ? 8 : 0,
|
||||
),
|
||||
child: MessageToolbar(
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
controller: widget.controller,
|
||||
textSelection: widget.textSelection,
|
||||
initialMode: widget.initialMode,
|
||||
),
|
||||
),
|
||||
],
|
||||
MessageToolbar(
|
||||
pangeaMessageEvent: widget._pangeaMessageEvent,
|
||||
overLayController: this,
|
||||
),
|
||||
Message(
|
||||
widget.event,
|
||||
onSwipe: () => {},
|
||||
onInfoTab: (_) => {},
|
||||
onAvatarTab: (_) => {},
|
||||
scrollToEventId: (_) => {},
|
||||
onSelect: (_) => {},
|
||||
immersionMode: widget.controller.choreographer.immersionMode,
|
||||
controller: widget.controller,
|
||||
timeline: widget.controller.timeline!,
|
||||
isOverlay: true,
|
||||
animateIn: false,
|
||||
nextEvent: widget.nextEvent,
|
||||
previousEvent: widget.prevEvent,
|
||||
OverlayMessage(
|
||||
pangeaMessageEvent,
|
||||
immersionMode: widget.chatController.choreographer.immersionMode,
|
||||
controller: widget.chatController,
|
||||
overlayController: this,
|
||||
nextEvent: widget._nextEvent,
|
||||
prevEvent: widget._prevEvent,
|
||||
timeline: widget.chatController.timeline!,
|
||||
messageWidth: messageSize!.width,
|
||||
),
|
||||
ToolbarButtons(
|
||||
overlayController: this,
|
||||
width: 250,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
|
||||
final columnOffset = FluffyThemes.isColumnMode(context)
|
||||
? FluffyThemes.columnWidth + FluffyThemes.navRailWidth
|
||||
: 0;
|
||||
|
||||
final double leftPadding = widget._pangeaMessageEvent.ownMessage
|
||||
? extraChatSpace
|
||||
: messageOffset!.dx - horizontalPadding - columnOffset;
|
||||
|
||||
final double rightPadding = widget._pangeaMessageEvent.ownMessage
|
||||
? screenWidth -
|
||||
messageOffset!.dx -
|
||||
messageSize!.width -
|
||||
horizontalPadding
|
||||
: extraChatSpace;
|
||||
|
||||
final positionedOverlayMessage = _overlayPositionAnimation == null
|
||||
? Positioned(
|
||||
left: 0,
|
||||
right: showDetails ? FluffyThemes.columnWidth : 0,
|
||||
bottom: screenHeight - messageOffset!.dy - messageSize!.height,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: overlayMessage,
|
||||
),
|
||||
left: leftPadding,
|
||||
right: rightPadding,
|
||||
bottom: screenHeight -
|
||||
messageOffset!.dy -
|
||||
messageSize!.height -
|
||||
toolbarButtonsHeight,
|
||||
child: overlayMessage,
|
||||
)
|
||||
: AnimatedBuilder(
|
||||
animation: _overlayPositionAnimation!,
|
||||
builder: (context, child) {
|
||||
return Positioned(
|
||||
left: 0,
|
||||
right: showDetails ? FluffyThemes.columnWidth : 0,
|
||||
left: leftPadding,
|
||||
right: rightPadding,
|
||||
bottom: _overlayPositionAnimation!.value,
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: overlayMessage,
|
||||
),
|
||||
child: overlayMessage,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: FluffyThemes.isColumnMode(context) ? 8.0 : 0.0,
|
||||
right: FluffyThemes.isColumnMode(context) ? 8.0 : 0.0,
|
||||
left: horizontalPadding,
|
||||
right: horizontalPadding,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
|
|
@ -240,7 +426,7 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
OverlayFooter(controller: widget.controller),
|
||||
OverlayFooter(controller: widget.chatController),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -252,10 +438,32 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
),
|
||||
),
|
||||
Material(
|
||||
child: OverlayHeader(controller: widget.controller),
|
||||
child: OverlayHeader(controller: widget.chatController),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MessagePadding extends StatelessWidget {
|
||||
const MessagePadding({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.pangeaMessageEvent,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: pangeaMessageEvent.ownMessage ? 0 : Avatar.defaultSize + 16,
|
||||
right: pangeaMessageEvent.ownMessage ? 8 : 0,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ 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/inline_tooltip.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.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';
|
||||
|
|
@ -66,13 +68,6 @@ class MessageSpeechToTextCardState extends State<MessageSpeechToTextCard> {
|
|||
}
|
||||
|
||||
void closeHint() {
|
||||
MatrixState.pangeaController.instructions.turnOffInstruction(
|
||||
InlineInstructions.speechToText.toString(),
|
||||
);
|
||||
MatrixState.pangeaController.instructions.updateEnableInstructions(
|
||||
InlineInstructions.speechToText.toString(),
|
||||
true,
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
|
|
@ -164,54 +159,46 @@ class MessageSpeechToTextCardState extends State<MessageSpeechToTextCard> {
|
|||
final int total = words * accuracy;
|
||||
|
||||
//TODO: find better icons
|
||||
return Column(
|
||||
children: [
|
||||
RichText(
|
||||
text: _buildTranscriptText(context),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// IconNumberWidget(
|
||||
// icon: Icons.abc,
|
||||
// number: (selectedToken == null ? words : 1).toString(),
|
||||
// toolTip: L10n.of(context)!.words,
|
||||
// ),
|
||||
IconNumberWidget(
|
||||
icon: Symbols.target,
|
||||
number:
|
||||
"${selectedToken?.confidence ?? speechToTextResponse!.transcript.confidence}%",
|
||||
toolTip: L10n.of(context)!.accuracy,
|
||||
onPressed: () => MatrixState.pangeaController.instructions
|
||||
.showInstructionsPopup(
|
||||
context,
|
||||
InstructionsEnum.tooltipInstructions,
|
||||
widget.messageEvent.eventId,
|
||||
true,
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(minHeight: minCardHeight),
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
RichText(
|
||||
text: _buildTranscriptText(context),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// IconNumberWidget(
|
||||
// icon: Icons.abc,
|
||||
// number: (selectedToken == null ? words : 1).toString(),
|
||||
// toolTip: L10n.of(context)!.words,
|
||||
// ),
|
||||
IconNumberWidget(
|
||||
icon: Symbols.target,
|
||||
number:
|
||||
"${selectedToken?.confidence ?? speechToTextResponse!.transcript.confidence}%",
|
||||
toolTip: L10n.of(context)!.accuracy,
|
||||
),
|
||||
),
|
||||
IconNumberWidget(
|
||||
icon: Icons.speed,
|
||||
number:
|
||||
wordsPerMinuteString != null ? "$wordsPerMinuteString" : "??",
|
||||
toolTip: L10n.of(context)!.wordsPerMinute,
|
||||
onPressed: () => MatrixState.pangeaController.instructions
|
||||
.showInstructionsPopup(
|
||||
context,
|
||||
InstructionsEnum.tooltipInstructions,
|
||||
widget.messageEvent.eventId,
|
||||
true,
|
||||
IconNumberWidget(
|
||||
icon: Icons.speed,
|
||||
number: wordsPerMinuteString != null
|
||||
? "$wordsPerMinuteString"
|
||||
: "??",
|
||||
toolTip: L10n.of(context)!.wordsPerMinute,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
MatrixState.pangeaController.instructions.getInstructionInlineTooltip(
|
||||
context,
|
||||
InlineInstructions.speechToText,
|
||||
closeHint,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
InlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.speechToText,
|
||||
onClose: () => setState(() => {}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
/// Contains information about the text currently being shown in a
|
||||
/// toolbar overlay message and any selection within that text.
|
||||
/// The ChatController contains one instance of this class, and it's values
|
||||
/// should be updated each time an overlay is openned or closed, or when
|
||||
/// an overlay's text selection changes.
|
||||
class MessageTextSelection {
|
||||
/// The currently selected text in the overlay message.
|
||||
String? selectedText;
|
||||
|
||||
/// The full text displayed in the overlay message.
|
||||
String? messageText;
|
||||
|
||||
/// A stream that emits the currently selected text whenever it changes.
|
||||
final StreamController<String?> selectionStream =
|
||||
StreamController<String?>.broadcast();
|
||||
|
||||
/// Sets messageText to match the text currently being displayed in the overlay.
|
||||
/// Text in messages is displayed in a variety of ways, i.e., direct message content,
|
||||
/// translation, HTML rendered message, etc. This method should be called wherever the
|
||||
/// text displayed in the overlay is determined.
|
||||
void setMessageText(String text) => messageText = text;
|
||||
|
||||
/// Clears the messageText value. Called when the message selection overlay is closed.
|
||||
void clearMessageText() => messageText = null;
|
||||
|
||||
/// Updates the selectedText value and emits it to the selectionStream.
|
||||
void onSelection(String? text) {
|
||||
text == null || text.isEmpty ? selectedText = null : selectedText = text;
|
||||
selectionStream.add(selectedText);
|
||||
}
|
||||
|
||||
/// Returns the index of the selected text within the message text.
|
||||
/// If the selected text is not found, returns null.
|
||||
int? get offset {
|
||||
if (selectedText == null || messageText == null) return null;
|
||||
final index = messageText!.indexOf(selectedText!);
|
||||
return index > -1 ? index : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +1,32 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_audio_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_speech_to_text_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/select_to_define.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
const double minCardHeight = 70;
|
||||
|
||||
class MessageToolbar extends StatefulWidget {
|
||||
final MessageTextSelection textSelection;
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final ChatController controller;
|
||||
final MessageMode? initialMode;
|
||||
final MessageOverlayController overLayController;
|
||||
|
||||
const MessageToolbar({
|
||||
super.key,
|
||||
required this.textSelection,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.controller,
|
||||
this.initialMode,
|
||||
required this.overLayController,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -37,289 +34,119 @@ class MessageToolbar extends StatefulWidget {
|
|||
}
|
||||
|
||||
class MessageToolbarState extends State<MessageToolbar> {
|
||||
Widget? toolbarContent;
|
||||
MessageMode? currentMode;
|
||||
bool updatingMode = false;
|
||||
late StreamSubscription<String?> selectionStream;
|
||||
|
||||
void updateMode(MessageMode newMode) {
|
||||
//Early exit from the function if the widget has been unmounted to prevent updates on an inactive widget.
|
||||
if (!mounted) return;
|
||||
if (updatingMode) return;
|
||||
debugPrint("updating toolbar mode");
|
||||
final bool subscribed =
|
||||
MatrixState.pangeaController.subscriptionController.isSubscribed;
|
||||
|
||||
if (!newMode.isValidMode(widget.pangeaMessageEvent.event)) {
|
||||
ErrorHandler.logError(
|
||||
e: "Invalid mode for event",
|
||||
s: StackTrace.current,
|
||||
data: {
|
||||
"newMode": newMode,
|
||||
"event": widget.pangeaMessageEvent.event,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// if there is an uncompleted activity, then show that
|
||||
// we don't want the user to user the tools to get the answer :P
|
||||
if (widget.pangeaMessageEvent.hasUncompletedActivity) {
|
||||
newMode = MessageMode.practiceActivity;
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
currentMode = newMode;
|
||||
updatingMode = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (!subscribed) {
|
||||
toolbarContent = MessageUnsubscribedCard(
|
||||
languageTool: newMode.title(context),
|
||||
mode: newMode,
|
||||
controller: this,
|
||||
);
|
||||
} else {
|
||||
switch (currentMode) {
|
||||
case MessageMode.translation:
|
||||
showTranslation();
|
||||
break;
|
||||
case MessageMode.textToSpeech:
|
||||
showTextToSpeech();
|
||||
break;
|
||||
case MessageMode.speechToText:
|
||||
showSpeechToText();
|
||||
break;
|
||||
case MessageMode.definition:
|
||||
showDefinition();
|
||||
break;
|
||||
case MessageMode.practiceActivity:
|
||||
showPracticeActivity();
|
||||
break;
|
||||
default:
|
||||
ErrorHandler.logError(
|
||||
e: "Invalid toolbar mode",
|
||||
s: StackTrace.current,
|
||||
data: {"newMode": newMode},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
updatingMode = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void showTranslation() {
|
||||
debugPrint("show translation");
|
||||
toolbarContent = MessageTranslationCard(
|
||||
messageEvent: widget.pangeaMessageEvent,
|
||||
immersionMode: widget.controller.choreographer.immersionMode,
|
||||
selection: widget.textSelection,
|
||||
);
|
||||
}
|
||||
|
||||
void showTextToSpeech() {
|
||||
debugPrint("show text to speech");
|
||||
toolbarContent = MessageAudioCard(
|
||||
messageEvent: widget.pangeaMessageEvent,
|
||||
);
|
||||
}
|
||||
|
||||
void showSpeechToText() {
|
||||
debugPrint("show speech to text");
|
||||
toolbarContent = MessageSpeechToTextCard(
|
||||
messageEvent: widget.pangeaMessageEvent,
|
||||
);
|
||||
}
|
||||
|
||||
void showDefinition() {
|
||||
debugPrint("show definition");
|
||||
if (widget.textSelection.selectedText == null ||
|
||||
widget.textSelection.messageText == null ||
|
||||
widget.textSelection.selectedText!.isEmpty) {
|
||||
toolbarContent = const SelectToDefine();
|
||||
return;
|
||||
}
|
||||
|
||||
toolbarContent = WordDataCard(
|
||||
word: widget.textSelection.selectedText!,
|
||||
wordLang: widget.pangeaMessageEvent.messageDisplayLangCode,
|
||||
fullText: widget.textSelection.messageText!,
|
||||
fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode,
|
||||
hasInfo: true,
|
||||
room: widget.controller.room,
|
||||
);
|
||||
}
|
||||
|
||||
void showPracticeActivity() {
|
||||
toolbarContent = PracticeActivityCard(
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
);
|
||||
}
|
||||
|
||||
void showImage() {}
|
||||
|
||||
void spellCheck() {}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.textSelection.selectedText = null;
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
if (widget.pangeaMessageEvent.isAudioMessage) {
|
||||
updateMode(MessageMode.speechToText);
|
||||
return;
|
||||
}
|
||||
Widget get toolbarContent {
|
||||
final bool subscribed =
|
||||
MatrixState.pangeaController.subscriptionController.isSubscribed;
|
||||
|
||||
if (widget.initialMode != null) {
|
||||
updateMode(widget.initialMode!);
|
||||
} else {
|
||||
MatrixState.pangeaController.userController.profile.userSettings
|
||||
.autoPlayMessages
|
||||
? updateMode(MessageMode.textToSpeech)
|
||||
: updateMode(MessageMode.translation);
|
||||
}
|
||||
});
|
||||
if (!subscribed) {
|
||||
return MessageUnsubscribedCard(
|
||||
controller: widget.overLayController,
|
||||
);
|
||||
}
|
||||
|
||||
Timer? timer;
|
||||
selectionStream =
|
||||
widget.textSelection.selectionStream.stream.listen((value) {
|
||||
timer?.cancel();
|
||||
timer = Timer(const Duration(milliseconds: 500), () {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
final MessageMode newMode = currentMode == MessageMode.definition
|
||||
? MessageMode.definition
|
||||
: MessageMode.translation;
|
||||
updateMode(newMode);
|
||||
} else if (currentMode != null) {
|
||||
updateMode(currentMode!);
|
||||
switch (widget.overLayController.toolbarMode) {
|
||||
case MessageMode.translation:
|
||||
return MessageTranslationCard(
|
||||
messageEvent: widget.pangeaMessageEvent,
|
||||
selection: widget.overLayController.selectedSpan,
|
||||
);
|
||||
case MessageMode.textToSpeech:
|
||||
return MessageAudioCard(
|
||||
messageEvent: widget.pangeaMessageEvent,
|
||||
overlayController: widget.overLayController,
|
||||
);
|
||||
case MessageMode.speechToText:
|
||||
return MessageSpeechToTextCard(
|
||||
messageEvent: widget.pangeaMessageEvent,
|
||||
);
|
||||
case MessageMode.definition:
|
||||
if (!widget.overLayController.isSelection) {
|
||||
return const SelectToDefine();
|
||||
} else {
|
||||
try {
|
||||
final selectedText = widget.overLayController.targetText;
|
||||
|
||||
return WordDataCard(
|
||||
word: selectedText,
|
||||
wordLang: widget.pangeaMessageEvent.messageDisplayLangCode,
|
||||
fullText: widget.pangeaMessageEvent.messageDisplayText,
|
||||
fullTextLang: widget.pangeaMessageEvent.messageDisplayLangCode,
|
||||
hasInfo: true,
|
||||
room: widget.overLayController.widget.chatController.room,
|
||||
);
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: "Error in WordDataCard",
|
||||
s: s,
|
||||
data: {
|
||||
"word": widget.overLayController.targetText,
|
||||
"fullText": widget.pangeaMessageEvent.messageDisplayText,
|
||||
},
|
||||
);
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
case MessageMode.practiceActivity:
|
||||
return PracticeActivityCard(
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
overlayController: widget.overLayController,
|
||||
);
|
||||
default:
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: "Invalid toolbar mode",
|
||||
s: StackTrace.current,
|
||||
data: {"newMode": widget.overLayController.toolbarMode},
|
||||
);
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
selectionStream.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final buttonRow = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: MessageMode.values
|
||||
.map(
|
||||
(mode) => mode.isValidMode(widget.pangeaMessageEvent.event)
|
||||
? Tooltip(
|
||||
message: mode.tooltip(context),
|
||||
child: IconButton(
|
||||
icon: Icon(mode.icon),
|
||||
color: mode.iconColor(
|
||||
widget.pangeaMessageEvent,
|
||||
currentMode,
|
||||
context,
|
||||
),
|
||||
onPressed: () => updateMode(mode),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
return Material(
|
||||
key: MatrixState.pAnyState
|
||||
.layerLinkAndKey('${widget.pangeaMessageEvent.eventId}-toolbar')
|
||||
.key,
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: AppConfig.toolbarMaxHeight,
|
||||
maxWidth: 275,
|
||||
minWidth: 275,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(25),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (toolbarContent != null)
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: toolbarContent,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border.all(
|
||||
width: 2,
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: toolbarContent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
buttonRow,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarSelectionArea extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
final PangeaMessageEvent? pangeaMessageEvent;
|
||||
final bool isOverlay;
|
||||
final Widget child;
|
||||
final Event? nextEvent;
|
||||
final Event? prevEvent;
|
||||
|
||||
const ToolbarSelectionArea({
|
||||
required this.controller,
|
||||
this.pangeaMessageEvent,
|
||||
this.isOverlay = false,
|
||||
required this.child,
|
||||
this.nextEvent,
|
||||
this.prevEvent,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SelectionArea(
|
||||
onSelectionChanged: (SelectedContent? selection) {
|
||||
controller.textSelection.onSelection(selection?.plainText);
|
||||
},
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (pangeaMessageEvent != null && !isOverlay) {
|
||||
controller.showToolbar(
|
||||
pangeaMessageEvent!,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (pangeaMessageEvent != null && !isOverlay) {
|
||||
controller.showToolbar(
|
||||
pangeaMessageEvent!,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
109
lib/pangea/widgets/chat/message_toolbar_buttons.dart
Normal file
109
lib/pangea/widgets/chat/message_toolbar_buttons.dart
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ToolbarButtons extends StatelessWidget {
|
||||
final MessageOverlayController overlayController;
|
||||
final double width;
|
||||
|
||||
const ToolbarButtons({
|
||||
required this.overlayController,
|
||||
required this.width,
|
||||
super.key,
|
||||
});
|
||||
|
||||
PangeaMessageEvent get pangeaMessageEvent =>
|
||||
overlayController.pangeaMessageEvent;
|
||||
|
||||
List<MessageMode> get modes => MessageMode.values
|
||||
.where((mode) => mode.isValidMode(pangeaMessageEvent.event))
|
||||
.toList();
|
||||
|
||||
static const double iconWidth = 36.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double barWidth = width - iconWidth;
|
||||
|
||||
if (overlayController.pangeaMessageEvent.isAudioMessage) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: 50,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: width,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: MessageModeExtension.barAndLockedButtonColor(context),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: iconWidth / 2),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
height: 12,
|
||||
width: overlayController.isPracticeComplete
|
||||
? barWidth
|
||||
: min(
|
||||
barWidth,
|
||||
(barWidth / 3) *
|
||||
pangeaMessageEvent.numberOfActivitiesCompleted,
|
||||
),
|
||||
color: AppConfig.success,
|
||||
margin: const EdgeInsets.symmetric(horizontal: iconWidth / 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: modes
|
||||
.mapIndexed(
|
||||
(index, mode) => Tooltip(
|
||||
message: mode.tooltip(context),
|
||||
child: IconButton(
|
||||
iconSize: 20,
|
||||
icon: Icon(mode.icon),
|
||||
color: mode == overlayController.toolbarMode
|
||||
? Colors.white
|
||||
: null,
|
||||
isSelected: mode == overlayController.toolbarMode,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
mode.iconButtonColor(
|
||||
context,
|
||||
index,
|
||||
overlayController.toolbarMode,
|
||||
pangeaMessageEvent.numberOfActivitiesCompleted,
|
||||
overlayController.isPracticeComplete,
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: mode.isUnlocked(
|
||||
index,
|
||||
pangeaMessageEvent.numberOfActivitiesCompleted,
|
||||
overlayController.isPracticeComplete,
|
||||
)
|
||||
? () => overlayController.updateToolbarMode(mode)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
lib/pangea/widgets/chat/message_toolbar_selection_area.dart
Normal file
48
lib/pangea/widgets/chat/message_toolbar_selection_area.dart
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class ToolbarSelectionArea extends StatelessWidget {
|
||||
final ChatController controller;
|
||||
final PangeaMessageEvent? pangeaMessageEvent;
|
||||
final bool isOverlay;
|
||||
final Widget child;
|
||||
final Event? nextEvent;
|
||||
final Event? prevEvent;
|
||||
|
||||
const ToolbarSelectionArea({
|
||||
required this.controller,
|
||||
this.pangeaMessageEvent,
|
||||
this.isOverlay = false,
|
||||
required this.child,
|
||||
this.nextEvent,
|
||||
this.prevEvent,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (pangeaMessageEvent != null && !isOverlay) {
|
||||
controller.showToolbar(
|
||||
pangeaMessageEvent!,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (pangeaMessageEvent != null && !isOverlay) {
|
||||
controller.showToolbar(
|
||||
pangeaMessageEvent!,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/inline_tooltip.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_text_selection.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -13,13 +14,11 @@ import 'package:flutter/material.dart';
|
|||
|
||||
class MessageTranslationCard extends StatefulWidget {
|
||||
final PangeaMessageEvent messageEvent;
|
||||
final bool immersionMode;
|
||||
final MessageTextSelection selection;
|
||||
final PangeaTokenText? selection;
|
||||
|
||||
const MessageTranslationCard({
|
||||
super.key,
|
||||
required this.messageEvent,
|
||||
required this.immersionMode,
|
||||
required this.selection,
|
||||
});
|
||||
|
||||
|
|
@ -30,10 +29,25 @@ class MessageTranslationCard extends StatefulWidget {
|
|||
class MessageTranslationCardState extends State<MessageTranslationCard> {
|
||||
PangeaRepresentation? repEvent;
|
||||
String? selectionTranslation;
|
||||
String? oldSelectedText;
|
||||
bool _fetchingRepresentation = false;
|
||||
bool _fetchingTranslation = false;
|
||||
|
||||
Future<void> fetchRepresentation() async {
|
||||
@override
|
||||
void initState() {
|
||||
debugPrint('MessageTranslationCard initState');
|
||||
super.initState();
|
||||
loadTranslation();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MessageTranslationCard oldWidget) {
|
||||
if (oldWidget.selection != widget.selection) {
|
||||
debugPrint('selection changed');
|
||||
loadTranslation();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
Future<void> fetchRepresentationText() async {
|
||||
if (l1Code == null) return;
|
||||
|
||||
repEvent = widget.messageEvent
|
||||
|
|
@ -49,48 +63,48 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> translateSelection() async {
|
||||
if (widget.selection.selectedText == null ||
|
||||
l1Code == null ||
|
||||
l2Code == null ||
|
||||
widget.selection.messageText == null) {
|
||||
selectionTranslation = null;
|
||||
Future<void> fetchSelectedTextTranslation() async {
|
||||
if (!mounted) return;
|
||||
|
||||
final pangeaController = MatrixState.pangeaController;
|
||||
|
||||
if (!pangeaController.languageController.languagesSet) {
|
||||
selectionTranslation = widget.messageEvent.messageDisplayText;
|
||||
return;
|
||||
}
|
||||
|
||||
oldSelectedText = widget.selection.selectedText;
|
||||
final String accessToken =
|
||||
MatrixState.pangeaController.userController.accessToken;
|
||||
|
||||
final resp = await FullTextTranslationRepo.translate(
|
||||
accessToken: accessToken,
|
||||
final FullTextTranslationResponseModel res =
|
||||
await FullTextTranslationRepo.translate(
|
||||
accessToken: pangeaController.userController.accessToken,
|
||||
request: FullTextTranslationRequestModel(
|
||||
text: widget.selection.messageText!,
|
||||
text: widget.messageEvent.messageDisplayText,
|
||||
srcLang: widget.messageEvent.messageDisplayLangCode,
|
||||
tgtLang: l1Code!,
|
||||
offset: widget.selection?.offset,
|
||||
length: widget.selection?.length,
|
||||
userL1: l1Code!,
|
||||
userL2: l2Code!,
|
||||
srcLang: widget.messageEvent.messageDisplayLangCode,
|
||||
length: widget.selection.selectedText!.length,
|
||||
offset: widget.selection.offset,
|
||||
),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
selectionTranslation = resp.bestTranslation;
|
||||
}
|
||||
selectionTranslation = res.translations.first;
|
||||
}
|
||||
|
||||
Future<void> loadTranslation(Future<void> Function() future) async {
|
||||
Future<void> loadTranslation() async {
|
||||
if (!mounted) return;
|
||||
setState(() => _fetchingRepresentation = true);
|
||||
|
||||
setState(() => _fetchingTranslation = true);
|
||||
|
||||
try {
|
||||
await future();
|
||||
await (widget.selection != null
|
||||
? fetchSelectedTextTranslation()
|
||||
: fetchRepresentationText());
|
||||
} catch (err) {
|
||||
ErrorHandler.logError(e: err);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _fetchingRepresentation = false);
|
||||
setState(() => _fetchingTranslation = false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,68 +113,36 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
String? get l2Code =>
|
||||
MatrixState.pangeaController.languageController.activeL2Code();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loadTranslation(() async {
|
||||
final List<Future> futures = [];
|
||||
futures.add(fetchRepresentation());
|
||||
if (widget.selection.selectedText != null) {
|
||||
futures.add(translateSelection());
|
||||
}
|
||||
await Future.wait(futures);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MessageTranslationCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldSelectedText != widget.selection.selectedText) {
|
||||
loadTranslation(translateSelection);
|
||||
}
|
||||
}
|
||||
|
||||
void closeHint() {
|
||||
MatrixState.pangeaController.instructions.turnOffInstruction(
|
||||
InlineInstructions.l1Translation.toString(),
|
||||
);
|
||||
MatrixState.pangeaController.instructions.updateEnableInstructions(
|
||||
InlineInstructions.l1Translation.toString(),
|
||||
true,
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
/// Show warning if message's language code is user's L1
|
||||
/// or if translated text is same as original text.
|
||||
/// Warning does not show if was previously closed
|
||||
bool get showWarning {
|
||||
if (MatrixState.pangeaController.instructions.wereInstructionsTurnedOff(
|
||||
InlineInstructions.l1Translation.toString(),
|
||||
)) return false;
|
||||
|
||||
bool get notGoingToTranslate {
|
||||
final bool isWrittenInL1 =
|
||||
l1Code != null && widget.messageEvent.originalSent?.langCode == l1Code;
|
||||
final bool isTextIdentical = selectionTranslation != null &&
|
||||
widget.messageEvent.originalSent?.text == selectionTranslation;
|
||||
|
||||
return isWrittenInL1 || isTextIdentical;
|
||||
return (isWrittenInL1 || isTextIdentical);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_fetchingRepresentation &&
|
||||
debugPrint('MessageTranslationCard build');
|
||||
if (!_fetchingTranslation &&
|
||||
repEvent == null &&
|
||||
selectionTranslation == null) {
|
||||
return const CardErrorWidget();
|
||||
}
|
||||
|
||||
return Container(
|
||||
child: _fetchingRepresentation
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(minHeight: minCardHeight),
|
||||
alignment: Alignment.center,
|
||||
child: _fetchingTranslation
|
||||
? const ToolbarContentLoadingIndicator()
|
||||
: Column(
|
||||
children: [
|
||||
selectionTranslation != null
|
||||
widget.selection != null
|
||||
? Text(
|
||||
selectionTranslation!,
|
||||
style: BotStyle.text(context),
|
||||
|
|
@ -169,12 +151,17 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
repEvent!.text,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (showWarning)
|
||||
if (notGoingToTranslate && widget.selection == null)
|
||||
InlineTooltip(
|
||||
body: InlineInstructions.l1Translation.body(context),
|
||||
onClose: closeHint,
|
||||
instructionsEnum: InstructionsEnum.l1Translation,
|
||||
onClose: () => setState(() {}),
|
||||
),
|
||||
if (widget.selection != null)
|
||||
InlineTooltip(
|
||||
instructionsEnum: InstructionsEnum.clickAgainToDeselect,
|
||||
onClose: () => setState(() {}),
|
||||
),
|
||||
// if (widget.selection != null)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,15 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import '../../enum/message_mode_enum.dart';
|
||||
|
||||
class MessageUnsubscribedCard extends StatelessWidget {
|
||||
final String languageTool;
|
||||
final MessageMode mode;
|
||||
final MessageToolbarState controller;
|
||||
final MessageOverlayController controller;
|
||||
|
||||
const MessageUnsubscribedCard({
|
||||
super.key,
|
||||
required this.languageTool,
|
||||
required this.mode,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
|
|
@ -24,42 +18,52 @@ class MessageUnsubscribedCard extends StatelessWidget {
|
|||
final bool inTrialWindow =
|
||||
MatrixState.pangeaController.userController.inTrialWindow;
|
||||
|
||||
void onButtonPress() {
|
||||
if (inTrialWindow) {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.activateNewUserTrial();
|
||||
controller.updateMode(mode);
|
||||
} else {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.showPaywall(context);
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
style: BotStyle.text(context),
|
||||
"${L10n.of(context)!.subscribedToUnlockTools} $languageTool",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: onButtonPress,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
(AppConfig.primaryColor).withOpacity(0.1),
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
style: BotStyle.text(context),
|
||||
L10n.of(context)!.subscribedToUnlockTools,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (inTrialWindow) ...[
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.activateNewUserTrial();
|
||||
controller.updateToolbarMode(controller.toolbarMode);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
(AppConfig.primaryColor).withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
child: Text(L10n.of(context)!.activateTrial),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
inTrialWindow
|
||||
? L10n.of(context)!.activateTrial
|
||||
: L10n.of(context)!.getAccess,
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.showPaywall(context);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
(AppConfig.primaryColor).withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
child: Text(L10n.of(context)!.getAccess),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
116
lib/pangea/widgets/chat/overlay_message.dart
Normal file
116
lib/pangea/widgets/chat/overlay_message.dart
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
import 'package:fluffychat/pages/chat/events/message_content.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
class OverlayMessage extends StatelessWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
final ChatController controller;
|
||||
final Event? nextEvent;
|
||||
final Event? prevEvent;
|
||||
final Timeline timeline;
|
||||
final bool immersionMode;
|
||||
final double messageWidth;
|
||||
|
||||
const OverlayMessage(
|
||||
this.pangeaMessageEvent, {
|
||||
this.immersionMode = false,
|
||||
required this.overlayController,
|
||||
required this.controller,
|
||||
required this.timeline,
|
||||
required this.messageWidth,
|
||||
this.nextEvent,
|
||||
this.prevEvent,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final bool ownMessage =
|
||||
pangeaMessageEvent.event.senderId == Matrix.of(context).client.userID;
|
||||
|
||||
final displayTime =
|
||||
pangeaMessageEvent.event.type == EventTypes.RoomCreate ||
|
||||
nextEvent == null ||
|
||||
!pangeaMessageEvent.event.originServerTs
|
||||
.sameEnvironment(nextEvent!.originServerTs);
|
||||
|
||||
final nextEventSameSender = nextEvent != null &&
|
||||
{
|
||||
EventTypes.Message,
|
||||
EventTypes.Sticker,
|
||||
EventTypes.Encrypted,
|
||||
}.contains(nextEvent!.type) &&
|
||||
nextEvent!.senderId == pangeaMessageEvent.event.senderId &&
|
||||
!displayTime;
|
||||
|
||||
final previousEventSameSender = prevEvent != null &&
|
||||
{
|
||||
EventTypes.Message,
|
||||
EventTypes.Sticker,
|
||||
EventTypes.Encrypted,
|
||||
}.contains(prevEvent!.type) &&
|
||||
prevEvent!.senderId == pangeaMessageEvent.event.senderId &&
|
||||
prevEvent!.originServerTs
|
||||
.sameEnvironment(pangeaMessageEvent.event.originServerTs);
|
||||
|
||||
const hardCorner = Radius.circular(4);
|
||||
const roundedCorner = Radius.circular(AppConfig.borderRadius);
|
||||
final borderRadius = BorderRadius.only(
|
||||
topLeft: !ownMessage && nextEventSameSender ? hardCorner : roundedCorner,
|
||||
topRight: ownMessage && nextEventSameSender ? hardCorner : roundedCorner,
|
||||
bottomLeft:
|
||||
!ownMessage && previousEventSameSender ? hardCorner : roundedCorner,
|
||||
bottomRight:
|
||||
ownMessage && previousEventSameSender ? hardCorner : roundedCorner,
|
||||
);
|
||||
|
||||
final displayEvent = pangeaMessageEvent.event.getDisplayEvent(timeline);
|
||||
var color = theme.colorScheme.surfaceContainerHighest;
|
||||
if (ownMessage) {
|
||||
color = displayEvent.status.isError
|
||||
? Colors.redAccent
|
||||
: theme.colorScheme.primary;
|
||||
}
|
||||
|
||||
return Material(
|
||||
color: color,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
width: messageWidth,
|
||||
child: MessageContent(
|
||||
pangeaMessageEvent.event,
|
||||
textColor: ownMessage
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurface,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
overlayController: overlayController,
|
||||
controller: controller,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
147
lib/pangea/widgets/chat/overlay_message_text.dart
Normal file
147
lib/pangea/widgets/chat/overlay_message_text.dart
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import 'package:fluffychat/config/app_config.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/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class OverlayMessageText extends StatefulWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
|
||||
const OverlayMessageText({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.overlayController,
|
||||
});
|
||||
|
||||
@override
|
||||
OverlayMessageTextState createState() => OverlayMessageTextState();
|
||||
}
|
||||
|
||||
class OverlayMessageTextState extends State<OverlayMessageText> {
|
||||
final PangeaController pangeaController = MatrixState.pangeaController;
|
||||
List<PangeaToken>? tokens;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
tokens = widget.pangeaMessageEvent.originalSent?.tokens;
|
||||
if (widget.pangeaMessageEvent.originalSent != null && tokens == null) {
|
||||
widget.pangeaMessageEvent.originalSent!
|
||||
.tokensGlobal(context)
|
||||
.then((tokens) {
|
||||
// this isn't currently working because originalSent's _event is null
|
||||
setState(() => this.tokens = tokens);
|
||||
});
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final ownMessage = widget.pangeaMessageEvent.event.senderId ==
|
||||
Matrix.of(context).client.userID;
|
||||
|
||||
final style = TextStyle(
|
||||
color: ownMessage
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurface,
|
||||
height: 1.3,
|
||||
fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor,
|
||||
);
|
||||
|
||||
if (tokens == null || tokens!.isEmpty) {
|
||||
return Text(
|
||||
widget.pangeaMessageEvent.event.calcLocalizedBodyFallback(
|
||||
MatrixLocals(L10n.of(context)!),
|
||||
hideReply: true,
|
||||
),
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
|
||||
int lastEnd = 0;
|
||||
final List<TokenPosition> tokenPositions = [];
|
||||
|
||||
for (int i = 0; i < tokens!.length; i++) {
|
||||
final token = tokens![i];
|
||||
final start = token.start;
|
||||
final end = token.end;
|
||||
|
||||
if (lastEnd < start) {
|
||||
tokenPositions.add(TokenPosition(start: lastEnd, end: start));
|
||||
}
|
||||
|
||||
tokenPositions.add(
|
||||
TokenPosition(
|
||||
start: start,
|
||||
end: end,
|
||||
tokenIndex: i,
|
||||
token: token,
|
||||
),
|
||||
);
|
||||
lastEnd = end;
|
||||
}
|
||||
|
||||
//TODO - take out of build function of every message
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
children: tokenPositions.map((tokenPosition) {
|
||||
if (tokenPosition.token != null) {
|
||||
final isSelected =
|
||||
widget.overlayController.isTokenSelected(tokenPosition.token!);
|
||||
return TextSpan(
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
debugPrint(
|
||||
'tokenPosition.tokenIndex: ${tokenPosition.tokenIndex}',
|
||||
);
|
||||
widget.overlayController.onClickOverlayMessageToken(
|
||||
tokenPosition.token!,
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
text: tokenPosition.token!.text.content,
|
||||
style: style.merge(
|
||||
TextStyle(
|
||||
backgroundColor: isSelected
|
||||
? Theme.of(context).brightness == Brightness.light
|
||||
? Colors.black.withOpacity(0.4)
|
||||
: Colors.white.withOpacity(0.4)
|
||||
: Colors.transparent,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return TextSpan(
|
||||
text: widget.pangeaMessageEvent.event.body.substring(
|
||||
tokenPosition.start,
|
||||
tokenPosition.end,
|
||||
),
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TokenPosition {
|
||||
final int start;
|
||||
final int end;
|
||||
final PangeaToken? token;
|
||||
final int tokenIndex;
|
||||
|
||||
const TokenPosition({
|
||||
required this.start,
|
||||
required this.end,
|
||||
this.token,
|
||||
this.tokenIndex = -1,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ToolbarContentLoadingIndicator extends StatelessWidget {
|
||||
|
|
@ -7,13 +8,18 @@ class ToolbarContentLoadingIndicator extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(minHeight: minCardHeight),
|
||||
alignment: Alignment.center,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -35,24 +34,25 @@ class AnalyticsPopup extends StatelessWidget {
|
|||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: constructsModel.constructs.isEmpty
|
||||
child: constructsModel.constructList.isEmpty
|
||||
? Center(
|
||||
child: Text(L10n.of(context)!.noDataFound),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: constructsModel.constructs.length,
|
||||
itemCount: constructsModel.constructList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Tooltip(
|
||||
message:
|
||||
"${constructsModel.constructs[index].points} / ${constructsModel.type.maxXPPerLemma}",
|
||||
"${constructsModel.constructList[index].points} / ${constructsModel.maxXPPerLemma}",
|
||||
child: ListTile(
|
||||
onTap: () {},
|
||||
title: Text(
|
||||
constructsModel.constructs[index].lemma,
|
||||
constructsModel.constructList[index].lemma,
|
||||
),
|
||||
subtitle: LinearProgressIndicator(
|
||||
value: constructsModel.constructs[index].points /
|
||||
constructsModel.type.maxXPPerLemma,
|
||||
value:
|
||||
constructsModel.constructList[index].points /
|
||||
constructsModel.maxXPPerLemma,
|
||||
minHeight: 20,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(AppConfig.borderRadius),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/constants/analytics_constants.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
|
||||
|
|
@ -13,6 +12,7 @@ import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/analytics_
|
|||
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/progress_indicator.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
|
@ -59,7 +59,7 @@ class LearningProgressIndicatorsState
|
|||
_pangeaController.analytics.locallyCachedConstructs,
|
||||
);
|
||||
int get serverXP => currentXP - localXP;
|
||||
int get level => currentXP ~/ AnalyticsConstants.xpPerLevel;
|
||||
int get level => _pangeaController.analytics.level;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -142,12 +142,17 @@ class LearningProgressIndicatorsState
|
|||
final progressBar = ProgressBar(
|
||||
levelBars: [
|
||||
LevelBarDetails(
|
||||
fillColor: const Color.fromARGB(255, 0, 190, 83),
|
||||
fillColor: kDebugMode
|
||||
? const Color.fromARGB(255, 0, 190, 83)
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
currentPoints: currentXP,
|
||||
width: levelBarWidth * _pangeaController.analytics.levelProgress,
|
||||
),
|
||||
LevelBarDetails(
|
||||
fillColor: Theme.of(context).colorScheme.primary,
|
||||
currentPoints: serverXP,
|
||||
width:
|
||||
levelBarWidth * _pangeaController.analytics.serverLevelProgress,
|
||||
),
|
||||
],
|
||||
progressBarDetails: ProgressBarDetails(
|
||||
|
|
@ -239,15 +244,19 @@ class LearningProgressIndicatorsState
|
|||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 36,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(left: 16, right: 0, child: progressBar),
|
||||
Positioned(left: 0, child: levelBadge),
|
||||
],
|
||||
Center(
|
||||
child: SizedBox(
|
||||
height: 36,
|
||||
child: SizedBox(
|
||||
width: levelBarWidth + 16,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(left: 16, right: 0, child: progressBar),
|
||||
Positioned(left: 0, child: levelBadge),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class IconNumberWidget extends StatelessWidget {
|
|||
final Color? iconColor;
|
||||
final double? iconSize;
|
||||
final String? toolTip;
|
||||
final VoidCallback onPressed;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
const IconNumberWidget({
|
||||
super.key,
|
||||
|
|
@ -15,7 +15,7 @@ class IconNumberWidget extends StatelessWidget {
|
|||
this.toolTip,
|
||||
this.iconColor,
|
||||
this.iconSize,
|
||||
required this.onPressed,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
Widget _content(BuildContext context) {
|
||||
|
|
|
|||
89
lib/pangea/widgets/content_issue_button.dart
Normal file
89
lib/pangea/widgets/content_issue_button.dart
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import 'package:fluffychat/pangea/widgets/common/bot_face_svg.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class ContentIssueButton extends StatelessWidget {
|
||||
final bool isActive;
|
||||
final void Function(String) submitFeedback;
|
||||
|
||||
const ContentIssueButton({
|
||||
super.key,
|
||||
required this.isActive,
|
||||
required this.submitFeedback,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Opacity(
|
||||
opacity: 0.8, // Slight opacity
|
||||
child: Tooltip(
|
||||
message: L10n.of(context)!.reportContentIssueTitle,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.flag),
|
||||
iconSize: 16,
|
||||
onPressed: () {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
final TextEditingController feedbackController =
|
||||
TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
L10n.of(context)!.reportContentIssueTitle,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
content: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const BotFace(
|
||||
width: 60,
|
||||
expression: BotExpression.addled,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(L10n.of(context)!.reportContentIssueDescription),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextField(
|
||||
controller: feedbackController,
|
||||
decoration: InputDecoration(
|
||||
labelText: L10n.of(context)!.feedback,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
maxLines: 4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(); // Close the dialog
|
||||
},
|
||||
child: Text(L10n.of(context)!.cancel),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Call the additional callback function
|
||||
submitFeedback(feedbackController.text);
|
||||
Navigator.of(context).pop(); // Close the dialog
|
||||
},
|
||||
child: Text(L10n.of(context)!.submit),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:fluffychat/pangea/constants/bot_mode.dart';
|
||||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_custom_zone.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_text_adventure_zone.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'conversation_bot_discussion_zone.dart';
|
||||
|
|
@ -18,20 +18,18 @@ class ConversationBotModeDynamicZone extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final zoneMap = {
|
||||
'discussion': ConversationBotDiscussionZone(
|
||||
BotMode.discussion: ConversationBotDiscussionZone(
|
||||
initialBotOptions: initialBotOptions,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
"custom": ConversationBotCustomZone(
|
||||
initialBotOptions: initialBotOptions,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
// "conversation": const ConversationBotConversationZone(),
|
||||
"text_adventure": ConversationBotTextAdventureZone(
|
||||
BotMode.custom: ConversationBotCustomZone(
|
||||
initialBotOptions: initialBotOptions,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
};
|
||||
if (!zoneMap.containsKey(initialBotOptions.mode)) {
|
||||
return Container();
|
||||
}
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:fluffychat/pangea/constants/bot_mode.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
|
|
@ -14,13 +15,13 @@ class ConversationBotModeSelect extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Map<String, String> options = {
|
||||
"discussion":
|
||||
BotMode.discussion:
|
||||
L10n.of(context)!.conversationBotModeSelectOption_discussion,
|
||||
"custom": L10n.of(context)!.conversationBotModeSelectOption_custom,
|
||||
// "conversation":
|
||||
// L10n.of(context)!.conversationBotModeSelectOption_conversation,
|
||||
"text_adventure":
|
||||
L10n.of(context)!.conversationBotModeSelectOption_textAdventure,
|
||||
BotMode.custom: L10n.of(context)!.conversationBotModeSelectOption_custom,
|
||||
// BotMode.textAdventure:
|
||||
// L10n.of(context)!.conversationBotModeSelectOption_textAdventure,
|
||||
// BotMode.storyGame:
|
||||
// L10n.of(context)!.conversationBotModeSelectOption_storyGame,
|
||||
};
|
||||
|
||||
return Padding(
|
||||
|
|
@ -38,7 +39,7 @@ class ConversationBotModeSelect extends StatelessWidget {
|
|||
hint: Padding(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
child: Text(
|
||||
options[initialMode ?? "discussion"]!,
|
||||
options[initialMode ?? BotMode.discussion]!,
|
||||
style: const TextStyle().copyWith(
|
||||
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||
fontSize: 14,
|
||||
|
|
|
|||
|
|
@ -256,14 +256,16 @@ class ConversationBotSettingsState extends State<ConversationBotSettings> {
|
|||
},
|
||||
);
|
||||
if (confirm == true) {
|
||||
if (addBot) {
|
||||
await widget.room?.invite(BotName.byEnvironment);
|
||||
} else {
|
||||
await widget.room?.kick(BotName.byEnvironment);
|
||||
}
|
||||
updateBotOption(() {
|
||||
botOptions = botOptions;
|
||||
});
|
||||
final bool isBotRoomMember =
|
||||
await widget.room?.isBotRoom ?? false;
|
||||
if (addBot && !isBotRoomMember) {
|
||||
await widget.room?.invite(BotName.byEnvironment);
|
||||
} else if (!addBot && isBotRoomMember) {
|
||||
await widget.room?.kick(BotName.byEnvironment);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:fluffychat/pangea/constants/bot_mode.dart';
|
||||
import 'package:fluffychat/pangea/models/bot_options_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_dynamic_zone.dart';
|
||||
import 'package:fluffychat/pangea/widgets/conversation_bot/conversation_bot_mode_select.dart';
|
||||
|
|
@ -65,7 +66,7 @@ class ConversationBotSettingsFormState
|
|||
initialMode: botOptions.mode,
|
||||
onChanged: (String? mode) => {
|
||||
setState(() {
|
||||
botOptions.mode = mode ?? "discussion";
|
||||
botOptions.mode = mode ?? BotMode.discussion;
|
||||
}),
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/common/bot_face_svg.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/card_header.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -20,25 +21,30 @@ class CardErrorWidget extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final ErrorCopy errorCopy = ErrorCopy(context, error);
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CardHeader(
|
||||
text: errorCopy.title,
|
||||
botExpression: BotExpression.addled,
|
||||
onClose: () => choreographer?.onMatchError(
|
||||
cursorOffset: offset,
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(minHeight: minCardHeight),
|
||||
alignment: Alignment.center,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CardHeader(
|
||||
text: errorCopy.title,
|
||||
botExpression: BotExpression.addled,
|
||||
onClose: () => choreographer?.onMatchError(
|
||||
cursorOffset: offset,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10.0),
|
||||
Center(
|
||||
child: Text(
|
||||
errorCopy.body,
|
||||
style: BotStyle.text(context),
|
||||
const SizedBox(height: 10.0),
|
||||
Center(
|
||||
child: Text(
|
||||
errorCopy.body,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import 'package:fluffychat/pangea/enum/instructions_enum.dart';
|
|||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar_selection_area.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -67,9 +67,6 @@ class PangeaRichTextState extends State<PangeaRichText> {
|
|||
if (!mounted) return; // Early exit if the widget is no longer in the tree
|
||||
setState(() {
|
||||
textSpan = newTextSpan;
|
||||
if (widget.isOverlay) {
|
||||
widget.controller.textSelection.setMessageText(textSpan);
|
||||
}
|
||||
});
|
||||
} catch (error, stackTrace) {
|
||||
ErrorHandler.logError(
|
||||
|
|
|
|||
|
|
@ -21,69 +21,84 @@ class PaywallCard extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CardHeader(
|
||||
text: L10n.of(context)!.subscriptionPopupTitle,
|
||||
text: L10n.of(context)!.clickMessageTitle,
|
||||
botExpression: BotExpression.addled,
|
||||
onClose: () {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.dismissPaywall();
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(17),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context)!.subscriptionPopupDesc,
|
||||
L10n.of(context)!.subscribedToUnlockTools,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (inTrialWindow)
|
||||
Text(
|
||||
L10n.of(context)!.noPaymentInfo,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
// if (inTrialWindow)
|
||||
// Text(
|
||||
// L10n.of(context)!.noPaymentInfo,
|
||||
// style: BotStyle.text(context),
|
||||
// textAlign: TextAlign.center,
|
||||
// ),
|
||||
if (inTrialWindow) ...[
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.activateNewUserTrial();
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
(AppConfig.primaryColor).withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
child: Text(L10n.of(context)!.activateTrial),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15.0),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
inTrialWindow
|
||||
? MatrixState.pangeaController.subscriptionController
|
||||
.activateNewUserTrial()
|
||||
: MatrixState.pangeaController.subscriptionController
|
||||
.showPaywall(context);
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.showPaywall(context);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
(AppConfig.primaryColor).withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
inTrialWindow
|
||||
? L10n.of(context)!.activateTrial
|
||||
: L10n.of(context)!.seeOptions,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
AppConfig.primaryColor.withOpacity(0.1),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.dismissPaywall();
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
},
|
||||
child: Center(
|
||||
child: Text(L10n.of(context)!.continuedWithoutSubscription),
|
||||
),
|
||||
child: Text(L10n.of(context)!.getAccess),
|
||||
),
|
||||
),
|
||||
// const SizedBox(height: 5.0),
|
||||
// SizedBox(
|
||||
// width: double.infinity,
|
||||
// child: TextButton(
|
||||
// style: ButtonStyle(
|
||||
// backgroundColor: WidgetStateProperty.all<Color>(
|
||||
// AppConfig.primaryColor.withOpacity(0.1),
|
||||
// ),
|
||||
// ),
|
||||
// onPressed: () {
|
||||
// MatrixState.pangeaController.subscriptionController
|
||||
// .dismissPaywall();
|
||||
// MatrixState.pAnyState.closeOverlay();
|
||||
// },
|
||||
// child: Center(
|
||||
// child: Text(L10n.of(context)!.continuedWithoutSubscription),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ class SpanCardState extends State<SpanCard> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> onChoiceSelect(int index) async {
|
||||
Future<void> onChoiceSelect(String value, int index) async {
|
||||
selectedChoiceIndex = index;
|
||||
if (selectedChoice != null) {
|
||||
if (!selectedChoice!.selected) {
|
||||
|
|
@ -143,9 +143,9 @@ class SpanCardState extends State<SpanCard> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the list of choices that are not selected
|
||||
/// Returns the list of distractor choices that are not selected
|
||||
List<SpanChoice>? get ignoredMatches => widget.scm.pangeaMatch?.match.choices
|
||||
?.where((choice) => !choice.selected)
|
||||
?.where((choice) => choice.isDistractor && !choice.selected)
|
||||
.toList();
|
||||
|
||||
/// Returns the list of tokens from choices that are not selected
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ class WhyButton extends StatelessWidget {
|
|||
return TextButton(
|
||||
onPressed: loading ? null : onPress,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all<Color>(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
AppConfig.primaryColor.withOpacity(0.1),
|
||||
),
|
||||
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
|
||||
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10), // Border radius
|
||||
side: const BorderSide(
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:fluffychat/pangea/models/language_model.dart';
|
|||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
|
@ -76,6 +77,7 @@ class WordDataCardController extends State<WordDataCard> {
|
|||
|
||||
@override
|
||||
void didUpdateWidget(covariant WordDataCard oldWidget) {
|
||||
// debugger(when: kDebugMode);
|
||||
if (oldWidget.word != widget.word) {
|
||||
if (!widget.hasInfo) {
|
||||
getContextualDefinition();
|
||||
|
|
@ -173,56 +175,61 @@ class WordDataCardView extends StatelessWidget {
|
|||
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
return Scrollbar(
|
||||
thumbVisibility: true,
|
||||
controller: scrollController,
|
||||
child: SingleChildScrollView(
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
constraints: const BoxConstraints(minHeight: minCardHeight),
|
||||
alignment: Alignment.center,
|
||||
child: Scrollbar(
|
||||
thumbVisibility: true,
|
||||
controller: scrollController,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (controller.widget.choiceFeedback != null)
|
||||
Text(
|
||||
controller.widget.choiceFeedback!,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
if (controller.wordData != null &&
|
||||
controller.wordNetError == null &&
|
||||
controller.activeL1 != null &&
|
||||
controller.activeL2 != null)
|
||||
WordNetInfo(
|
||||
wordData: controller.wordData!,
|
||||
activeL1: controller.activeL1!,
|
||||
activeL2: controller.activeL2!,
|
||||
),
|
||||
if (controller.isLoadingWordNet) const PCircular(),
|
||||
const SizedBox(height: 5.0),
|
||||
// if (controller.widget.hasInfo &&
|
||||
// !controller.isLoadingContextualDefinition &&
|
||||
// controller.contextualDefinitionRes == null)
|
||||
// Material(
|
||||
// type: MaterialType.transparency,
|
||||
// child: ListTile(
|
||||
// leading: const BotFace(
|
||||
// width: 40, expression: BotExpression.surprised),
|
||||
// title: Text(L10n.of(context)!.askPangeaBot),
|
||||
// onTap: controller.handleGetDefinitionButtonPress,
|
||||
// ),
|
||||
// ),
|
||||
if (controller.isLoadingContextualDefinition) const PCircular(),
|
||||
if (controller.contextualDefinitionRes != null)
|
||||
Text(
|
||||
controller.contextualDefinitionRes!.text,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
if (controller.definitionError != null)
|
||||
Text(
|
||||
L10n.of(context)!.sorryNoResults,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
],
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (controller.widget.choiceFeedback != null)
|
||||
Text(
|
||||
controller.widget.choiceFeedback!,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
const SizedBox(height: 5.0),
|
||||
if (controller.wordData != null &&
|
||||
controller.wordNetError == null &&
|
||||
controller.activeL1 != null &&
|
||||
controller.activeL2 != null)
|
||||
WordNetInfo(
|
||||
wordData: controller.wordData!,
|
||||
activeL1: controller.activeL1!,
|
||||
activeL2: controller.activeL2!,
|
||||
),
|
||||
if (controller.isLoadingWordNet) const PCircular(),
|
||||
const SizedBox(height: 5.0),
|
||||
// if (controller.widget.hasInfo &&
|
||||
// !controller.isLoadingContextualDefinition &&
|
||||
// controller.contextualDefinitionRes == null)
|
||||
// Material(
|
||||
// type: MaterialType.transparency,
|
||||
// child: ListTile(
|
||||
// leading: const BotFace(
|
||||
// width: 40, expression: BotExpression.surprised),
|
||||
// title: Text(L10n.of(context)!.askPangeaBot),
|
||||
// onTap: controller.handleGetDefinitionButtonPress,
|
||||
// ),
|
||||
// ),
|
||||
if (controller.isLoadingContextualDefinition) const PCircular(),
|
||||
if (controller.contextualDefinitionRes != null)
|
||||
Text(
|
||||
controller.contextualDefinitionRes!.text,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
if (controller.definitionError != null)
|
||||
Text(
|
||||
L10n.of(context)!.sorryNoResults,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -396,20 +403,3 @@ class PartOfSpeechBlock extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectToDefine extends StatelessWidget {
|
||||
const SelectToDefine({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
L10n.of(context)!.selectToDefine,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
|
@ -33,27 +32,29 @@ class GeneratePracticeActivityButton extends StatelessWidget {
|
|||
return;
|
||||
}
|
||||
|
||||
final PracticeActivityEvent? practiceActivityEvent = await MatrixState
|
||||
.pangeaController.practiceGenerationController
|
||||
.getPracticeActivity(
|
||||
PracticeActivityRequest(
|
||||
candidateMessages: [
|
||||
CandidateMessage(
|
||||
msgId: pangeaMessageEvent.eventId,
|
||||
roomId: pangeaMessageEvent.room.id,
|
||||
text:
|
||||
pangeaMessageEvent.representationByLanguage(l2Code)?.text ??
|
||||
pangeaMessageEvent.body,
|
||||
),
|
||||
],
|
||||
userIds: pangeaMessageEvent.room.client.userID != null
|
||||
? [pangeaMessageEvent.room.client.userID!]
|
||||
: null,
|
||||
),
|
||||
pangeaMessageEvent,
|
||||
);
|
||||
throw UnimplementedError();
|
||||
|
||||
onActivityGenerated(practiceActivityEvent);
|
||||
// final PracticeActivityEvent? practiceActivityEvent = await MatrixState
|
||||
// .pangeaController.practiceGenerationController
|
||||
// .getPracticeActivity(
|
||||
// MessageActivityRequest(
|
||||
// candidateMessages: [
|
||||
// CandidateMessage(
|
||||
// msgId: pangeaMessageEvent.eventId,
|
||||
// roomId: pangeaMessageEvent.room.id,
|
||||
// text:
|
||||
// pangeaMessageEvent.representationByLanguage(l2Code)?.text ??
|
||||
// pangeaMessageEvent.body,
|
||||
// ),
|
||||
// ],
|
||||
// userIds: pangeaMessageEvent.room.client.userID != null
|
||||
// ? [pangeaMessageEvent.room.client.userID!]
|
||||
// : null,
|
||||
// ),
|
||||
// pangeaMessageEvent,
|
||||
// );
|
||||
|
||||
// onActivityGenerated(practiceActivityEvent);
|
||||
},
|
||||
child: Text(L10n.of(context)!.practice),
|
||||
);
|
||||
|
|
@ -1,19 +1,24 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// The multiple choice activity view
|
||||
class MultipleChoiceActivity extends StatefulWidget {
|
||||
final MessagePracticeActivityCardState controller;
|
||||
final MessagePracticeActivityCardState practiceCardController;
|
||||
final PracticeActivityEvent? currentActivity;
|
||||
|
||||
const MultipleChoiceActivity({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.practiceCardController,
|
||||
required this.currentActivity,
|
||||
});
|
||||
|
||||
|
|
@ -25,60 +30,65 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
int? selectedChoiceIndex;
|
||||
|
||||
PracticeActivityRecordModel? get currentRecordModel =>
|
||||
widget.controller.currentRecordModel;
|
||||
|
||||
bool get isSubmitted =>
|
||||
widget.currentActivity?.userRecord?.record.latestResponse != null;
|
||||
widget.practiceCardController.currentCompletionRecord;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
setCompletionRecord();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MultipleChoiceActivity oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.currentActivity?.event.eventId !=
|
||||
widget.currentActivity?.event.eventId) {
|
||||
setCompletionRecord();
|
||||
if (widget.practiceCardController.currentCompletionRecord?.responses
|
||||
.isEmpty ??
|
||||
false) {
|
||||
setState(() => selectedChoiceIndex = null);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the completion record for the multiple choice activity.
|
||||
/// If the user record is null, it creates a new record model with the question
|
||||
/// from the current activity and sets the selected choice index to null.
|
||||
/// Otherwise, it sets the current model to the user record's record and
|
||||
/// determines the selected choice index.
|
||||
void setCompletionRecord() {
|
||||
if (widget.currentActivity?.userRecord?.record == null) {
|
||||
widget.controller.setCurrentModel(
|
||||
PracticeActivityRecordModel(
|
||||
question:
|
||||
widget.currentActivity?.practiceActivity.multipleChoice!.question,
|
||||
),
|
||||
);
|
||||
selectedChoiceIndex = null;
|
||||
} else {
|
||||
widget.controller
|
||||
.setCurrentModel(widget.currentActivity!.userRecord!.record);
|
||||
selectedChoiceIndex = widget
|
||||
.currentActivity?.practiceActivity.multipleChoice!
|
||||
.choiceIndex(currentRecordModel!.latestResponse!.text!);
|
||||
void updateChoice(String value, int index) {
|
||||
if (currentRecordModel?.hasTextResponse(value) ?? false) {
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void updateChoice(int index) {
|
||||
final bool isCorrect = widget
|
||||
.currentActivity!.practiceActivity.multipleChoice!
|
||||
.isCorrect(value, index);
|
||||
|
||||
currentRecordModel?.addResponse(
|
||||
text: widget.controller.currentActivity!.practiceActivity.multipleChoice!
|
||||
.choices[index],
|
||||
score: widget.controller.currentActivity!.practiceActivity.multipleChoice!
|
||||
.isCorrect(index)
|
||||
? 1
|
||||
: 0,
|
||||
text: value,
|
||||
score: isCorrect ? 1 : 0,
|
||||
);
|
||||
|
||||
if (currentRecordModel == null ||
|
||||
currentRecordModel!.latestResponse == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
MatrixState.pangeaController.myAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
// note - this maybe should be the activity event id
|
||||
eventId:
|
||||
widget.practiceCardController.widget.pangeaMessageEvent.eventId,
|
||||
roomId: widget.practiceCardController.widget.pangeaMessageEvent.room.id,
|
||||
constructs: currentRecordModel!.latestResponse!.toUses(
|
||||
widget.practiceCardController.currentActivity!.practiceActivity,
|
||||
widget.practiceCardController.metadata,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// If the selected choice is correct, send the record and get the next activity
|
||||
if (widget.currentActivity!.practiceActivity.multipleChoice!
|
||||
.isCorrect(value, index)) {
|
||||
widget.practiceCardController.onActivityFinish();
|
||||
}
|
||||
|
||||
setState(
|
||||
() => selectedChoiceIndex = index,
|
||||
);
|
||||
setState(() => selectedChoiceIndex = index);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -112,14 +122,15 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
.mapIndexed(
|
||||
(index, value) => Choice(
|
||||
text: value,
|
||||
color: selectedChoiceIndex == index
|
||||
color: currentRecordModel?.hasTextResponse(value) ?? false
|
||||
? practiceActivity.multipleChoice!.choiceColor(index)
|
||||
: null,
|
||||
isGold: practiceActivity.multipleChoice!.isCorrect(index),
|
||||
isGold: practiceActivity.multipleChoice!
|
||||
.isCorrect(value, index),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
isActive: !isSubmitted,
|
||||
isActive: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StarAnimationWidget extends StatefulWidget {
|
||||
const StarAnimationWidget({super.key});
|
||||
|
||||
@override
|
||||
_StarAnimationWidgetState createState() => _StarAnimationWidgetState();
|
||||
}
|
||||
|
||||
class _StarAnimationWidgetState extends State<StarAnimationWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _opacityAnimation;
|
||||
late Animation<double> _sizeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initialize the AnimationController
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 1), // Duration of the animation
|
||||
vsync: this,
|
||||
)..repeat(reverse: true); // Repeat the animation in reverse
|
||||
|
||||
// Define the opacity animation
|
||||
_opacityAnimation =
|
||||
Tween<double>(begin: 0.8, end: 1.0).animate(_controller);
|
||||
|
||||
// Define the size animation
|
||||
_sizeAnimation = Tween<double>(begin: 56.0, end: 60.0).animate(_controller);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Dispose of the controller to free resources
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
// Set constant height and width for the star container
|
||||
height: 60.0,
|
||||
width: 60.0,
|
||||
child: Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _opacityAnimation.value,
|
||||
child: Icon(
|
||||
Icons.star,
|
||||
color: Colors.amber,
|
||||
size: _sizeAnimation.value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GamifiedTextWidget extends StatelessWidget {
|
||||
final String userMessage;
|
||||
|
||||
const GamifiedTextWidget({super.key, required this.userMessage});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min, // Adjusts the size to fit children
|
||||
children: [
|
||||
const SizedBox(height: 10), // Spacing between the star and text
|
||||
// Star animation above the text
|
||||
const StarAnimationWidget(),
|
||||
const SizedBox(height: 10), // Spacing between the star and text
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 80,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
userMessage,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center, // Center-align the text
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,39 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/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/practice_activities.dart/message_activity_request.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_content.dart';
|
||||
import 'package:fluffychat/pangea/widgets/animations/gain_points.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:fluffychat/pangea/widgets/content_issue_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/no_more_practice_card.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/target_tokens_controller.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
/// The wrapper for practice activity content.
|
||||
/// Handles the activities assosiated with a message,
|
||||
/// Handles the activities associated with a message,
|
||||
/// their navigation, and the management of completion records
|
||||
class PracticeActivityCard extends StatefulWidget {
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
final MessageOverlayController overlayController;
|
||||
|
||||
const PracticeActivityCard({
|
||||
super.key,
|
||||
required this.pangeaMessageEvent,
|
||||
required this.overlayController,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
@ -29,159 +43,336 @@ class PracticeActivityCard extends StatefulWidget {
|
|||
|
||||
class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
||||
PracticeActivityEvent? currentActivity;
|
||||
PracticeActivityRecordModel? currentRecordModel;
|
||||
bool sending = false;
|
||||
PracticeActivityRecordModel? currentCompletionRecord;
|
||||
bool fetchingActivity = false;
|
||||
|
||||
// tracks the target tokens for the current message
|
||||
// in a separate controller to manage the state
|
||||
TargetTokensController targetTokensController = TargetTokensController();
|
||||
|
||||
List<PracticeActivityEvent> get practiceActivities =>
|
||||
widget.pangeaMessageEvent.practiceActivities;
|
||||
|
||||
int get practiceEventIndex => practiceActivities.indexWhere(
|
||||
(activity) => activity.event.eventId == currentActivity?.event.eventId,
|
||||
);
|
||||
|
||||
bool get isPrevEnabled =>
|
||||
practiceEventIndex > 0 &&
|
||||
practiceActivities.length > (practiceEventIndex - 1);
|
||||
|
||||
bool get isNextEnabled =>
|
||||
practiceEventIndex >= 0 &&
|
||||
practiceEventIndex < practiceActivities.length - 1;
|
||||
// Used to show an animation when the user completes an activity
|
||||
// while simultaneously fetching a new activity and not showing the loading spinner
|
||||
// until the appropriate time has passed to 'savor the joy'
|
||||
Duration appropriateTimeForJoy = const Duration(milliseconds: 1500);
|
||||
bool savoringTheJoy = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
setCurrentActivity();
|
||||
initialize();
|
||||
}
|
||||
|
||||
/// Initalizes the current activity.
|
||||
/// If the current activity hasn't been set yet, show the first
|
||||
/// uncompleted activity if there is one.
|
||||
/// If not, show the first activity
|
||||
void setCurrentActivity() {
|
||||
if (practiceActivities.isEmpty) return;
|
||||
void _updateFetchingActivity(bool value) {
|
||||
if (fetchingActivity == value) return;
|
||||
if (mounted) setState(() => fetchingActivity = value);
|
||||
}
|
||||
|
||||
void _setPracticeActivity(PracticeActivityEvent? activity) {
|
||||
//set elsewhere but just in case
|
||||
fetchingActivity = false;
|
||||
|
||||
currentActivity = activity;
|
||||
|
||||
if (activity == null) {
|
||||
widget.overlayController.exitPracticeFlow();
|
||||
return;
|
||||
}
|
||||
|
||||
//make new completion record
|
||||
currentCompletionRecord = PracticeActivityRecordModel(
|
||||
question: activity.practiceActivity.question,
|
||||
);
|
||||
|
||||
widget.overlayController.setSelectedSpan(activity.practiceActivity);
|
||||
}
|
||||
|
||||
/// Get an existing activity if there is one.
|
||||
/// If not, get a new activity from the server.
|
||||
Future<void> initialize() async {
|
||||
_setPracticeActivity(
|
||||
_fetchExistingIncompleteActivity() ?? await _fetchNewActivity(),
|
||||
);
|
||||
}
|
||||
|
||||
// if the user did the activity before but awhile ago and we don't have any
|
||||
// more target tokens, maybe we should give them the same activity again
|
||||
PracticeActivityEvent? _fetchExistingIncompleteActivity() {
|
||||
if (practiceActivities.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final List<PracticeActivityEvent> incompleteActivities =
|
||||
practiceActivities.where((element) => !element.isComplete).toList();
|
||||
currentActivity ??= incompleteActivities.isNotEmpty
|
||||
? incompleteActivities.first
|
||||
: practiceActivities.first;
|
||||
setState(() {});
|
||||
|
||||
// TODO - maybe check the user's xp for the tgtConstructs and decide if its relevant for them
|
||||
// however, maybe we'd like to go ahead and give them the activity to get some data on our xp?
|
||||
return incompleteActivities.firstOrNull;
|
||||
}
|
||||
|
||||
void setCurrentModel(PracticeActivityRecordModel? recordModel) {
|
||||
currentRecordModel = recordModel;
|
||||
}
|
||||
Future<PracticeActivityEvent?> _fetchNewActivity([
|
||||
ActivityQualityFeedback? activityFeedback,
|
||||
]) async {
|
||||
try {
|
||||
debugPrint('Fetching new activity');
|
||||
|
||||
/// Sets the current acitivity based on the given [direction].
|
||||
void navigateActivities(Direction direction) {
|
||||
final bool enableNavigation = (direction == Direction.f && isNextEnabled) ||
|
||||
(direction == Direction.b && isPrevEnabled);
|
||||
if (enableNavigation) {
|
||||
currentActivity = practiceActivities[direction == Direction.f
|
||||
? practiceEventIndex + 1
|
||||
: practiceEventIndex - 1];
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
_updateFetchingActivity(true);
|
||||
|
||||
/// Sends the current record model and activity to the server.
|
||||
/// If either the currentRecordModel or currentActivity is null, the method returns early.
|
||||
/// Sets the [sending] flag to true before sending the record and activity.
|
||||
/// Logs any errors that occur during the send operation.
|
||||
/// Sets the [sending] flag to false when the send operation is complete.
|
||||
void sendRecord() {
|
||||
if (currentRecordModel == null || currentActivity == null) return;
|
||||
setState(() => sending = true);
|
||||
MatrixState.pangeaController.activityRecordController
|
||||
.send(currentRecordModel!, currentActivity!)
|
||||
.catchError((error) {
|
||||
// target tokens can be empty if activities have been completed for each
|
||||
// it's set on initialization and then removed when each activity is completed
|
||||
if (!pangeaController.languageController.languagesSet) {
|
||||
debugger(when: kDebugMode);
|
||||
_updateFetchingActivity(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
debugger(when: kDebugMode);
|
||||
_updateFetchingActivity(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
final PracticeActivityEvent? ourNewActivity = await pangeaController
|
||||
.practiceGenerationController
|
||||
.getPracticeActivity(
|
||||
MessageActivityRequest(
|
||||
userL1: pangeaController.languageController.userL1!.langCode,
|
||||
userL2: pangeaController.languageController.userL2!.langCode,
|
||||
messageText: representation!.text,
|
||||
tokensWithXP: await targetTokensController.targetTokens(
|
||||
context,
|
||||
widget.pangeaMessageEvent,
|
||||
),
|
||||
messageId: widget.pangeaMessageEvent.eventId,
|
||||
existingActivities: practiceActivities
|
||||
.map((activity) => activity.activityRequestMetaData)
|
||||
.toList(),
|
||||
activityQualityFeedback: activityFeedback,
|
||||
),
|
||||
widget.pangeaMessageEvent,
|
||||
);
|
||||
|
||||
_updateFetchingActivity(false);
|
||||
|
||||
return ourNewActivity;
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: error,
|
||||
s: StackTrace.current,
|
||||
e: e,
|
||||
s: s,
|
||||
m: 'Failed to get new activity',
|
||||
data: {
|
||||
'recordModel': currentRecordModel?.toJson(),
|
||||
'practiceEvent': currentActivity?.event.toJson(),
|
||||
'activity': currentActivity,
|
||||
'record': currentCompletionRecord,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}).then((event) {
|
||||
// The record event is processed into construct uses for learning analytics, so if the
|
||||
// event went through without error, send it to analytics to be processed
|
||||
if (event != null && currentActivity != null) {
|
||||
MatrixState.pangeaController.myAnalytics.setState(
|
||||
}
|
||||
}
|
||||
|
||||
ConstructUseMetaData get metadata => ConstructUseMetaData(
|
||||
eventId: widget.pangeaMessageEvent.eventId,
|
||||
roomId: widget.pangeaMessageEvent.room.id,
|
||||
timeStamp: DateTime.now(),
|
||||
);
|
||||
|
||||
Future<void> _savorTheJoy() async {
|
||||
debugger(when: savoringTheJoy && kDebugMode);
|
||||
|
||||
setState(() => savoringTheJoy = true);
|
||||
|
||||
await Future.delayed(appropriateTimeForJoy);
|
||||
|
||||
if (mounted) setState(() => savoringTheJoy = false);
|
||||
}
|
||||
|
||||
/// Called when the user finishes an activity.
|
||||
/// Saves the completion record and sends it to the server.
|
||||
/// Fetches a new activity if there are any left to complete.
|
||||
/// Exits the practice flow if there are no more activities.
|
||||
void onActivityFinish() async {
|
||||
try {
|
||||
if (currentCompletionRecord == null || currentActivity == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
// update the target tokens with the new construct uses
|
||||
// NOTE - multiple choice activity is handling adding these to analytics
|
||||
await targetTokensController.updateTokensWithConstructs(
|
||||
currentCompletionRecord!.usesForAllResponses(
|
||||
currentActivity!.practiceActivity,
|
||||
metadata,
|
||||
),
|
||||
context,
|
||||
widget.pangeaMessageEvent,
|
||||
);
|
||||
|
||||
// save the record without awaiting to avoid blocking the UI
|
||||
// send a copy of the activity record to make sure its not overwritten by
|
||||
// the new activity
|
||||
MatrixState.pangeaController.activityRecordController
|
||||
.send(currentCompletionRecord!, currentActivity!)
|
||||
.catchError(
|
||||
(e, s) => ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
m: 'Failed to save record',
|
||||
data: {
|
||||
'record': currentCompletionRecord?.toJson(),
|
||||
'activity': currentActivity?.practiceActivity.toJson(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
widget.overlayController.onActivityFinish();
|
||||
|
||||
//
|
||||
final Iterable<dynamic> result = await Future.wait([
|
||||
_savorTheJoy(),
|
||||
_fetchNewActivity(),
|
||||
]);
|
||||
|
||||
_setPracticeActivity(result.last as PracticeActivityEvent?);
|
||||
} catch (e, s) {
|
||||
_setPracticeActivity(null);
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
m: 'Failed to get new activity',
|
||||
data: {
|
||||
'activity': currentActivity,
|
||||
'record': currentCompletionRecord,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// clear the current activity, record, and selection
|
||||
/// fetch a new activity, including the offending activity in the request
|
||||
void submitFeedback(String feedback) {
|
||||
if (currentActivity == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
_fetchNewActivity(
|
||||
ActivityQualityFeedback(
|
||||
feedbackText: feedback,
|
||||
badActivity: currentActivity!.practiceActivity,
|
||||
),
|
||||
).then((activity) {
|
||||
_setPracticeActivity(activity);
|
||||
}).catchError((onError) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: onError,
|
||||
m: 'Failed to get new activity',
|
||||
data: {
|
||||
'activity': currentActivity,
|
||||
'record': currentCompletionRecord,
|
||||
},
|
||||
);
|
||||
widget.overlayController.exitPracticeFlow();
|
||||
});
|
||||
|
||||
// clear the current activity and record
|
||||
currentActivity = null;
|
||||
currentCompletionRecord = null;
|
||||
}
|
||||
|
||||
RepresentationEvent? get representation =>
|
||||
widget.pangeaMessageEvent.originalSent;
|
||||
|
||||
String get messsageText => representation!.text;
|
||||
|
||||
PangeaController get pangeaController => MatrixState.pangeaController;
|
||||
|
||||
/// The widget that displays the current activity.
|
||||
/// If there is no current activity, the widget returns a sizedbox with a height of 80.
|
||||
/// If the activity type is multiple choice, the widget returns a MultipleChoiceActivity.
|
||||
/// If the activity type is unknown, the widget logs an error and returns a text widget with an error message.
|
||||
Widget get activityWidget {
|
||||
if (currentActivity == null) {
|
||||
// return sizedbox with height of 80
|
||||
return const SizedBox(height: 80);
|
||||
}
|
||||
switch (currentActivity!.practiceActivity.activityType) {
|
||||
case ActivityTypeEnum.multipleChoice:
|
||||
return MultipleChoiceActivity(
|
||||
practiceCardController: this,
|
||||
currentActivity: currentActivity,
|
||||
);
|
||||
default:
|
||||
ErrorHandler.logError(
|
||||
e: Exception('Unknown activity type'),
|
||||
m: 'Unknown activity type',
|
||||
data: {
|
||||
'eventID': widget.pangeaMessageEvent.eventId,
|
||||
'eventType': PangeaEventTypes.activityRecord,
|
||||
'roomID': event.room.id,
|
||||
'practiceActivity': currentActivity!,
|
||||
'recordModel': currentRecordModel!,
|
||||
'activityType': currentActivity!.practiceActivity.activityType,
|
||||
},
|
||||
);
|
||||
}
|
||||
}).whenComplete(() => setState(() => sending = false));
|
||||
return Text(
|
||||
L10n.of(context)!.oopsSomethingWentWrong,
|
||||
style: BotStyle.text(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String? get userMessage {
|
||||
if (!fetchingActivity && currentActivity == null) {
|
||||
return L10n.of(context)!.noActivitiesFound;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget navigationButtons = Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: isPrevEnabled ? 1.0 : 0,
|
||||
child: IconButton(
|
||||
onPressed:
|
||||
isPrevEnabled ? () => navigateActivities(Direction.b) : null,
|
||||
icon: const Icon(Icons.keyboard_arrow_left_outlined),
|
||||
tooltip: L10n.of(context)!.previous,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Opacity(
|
||||
opacity: currentActivity?.userRecord == null ? 1.0 : 0.5,
|
||||
child: sending
|
||||
? const CircularProgressIndicator.adaptive()
|
||||
: TextButton(
|
||||
onPressed:
|
||||
currentActivity?.userRecord == null ? sendRecord : null,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
AppConfig.primaryColor,
|
||||
),
|
||||
),
|
||||
child: Text(L10n.of(context)!.submit),
|
||||
),
|
||||
),
|
||||
),
|
||||
Opacity(
|
||||
opacity: isNextEnabled ? 1.0 : 0,
|
||||
child: IconButton(
|
||||
onPressed:
|
||||
isNextEnabled ? () => navigateActivities(Direction.f) : null,
|
||||
icon: const Icon(Icons.keyboard_arrow_right_outlined),
|
||||
tooltip: L10n.of(context)!.next,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (currentActivity == null || practiceActivities.isEmpty) {
|
||||
return Text(
|
||||
L10n.of(context)!.noActivitiesFound,
|
||||
style: BotStyle.text(context),
|
||||
);
|
||||
// return GeneratePracticeActivityButton(
|
||||
// pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
// onActivityGenerated: updatePracticeActivity,
|
||||
// );
|
||||
if (userMessage != null) {
|
||||
return GamifiedTextWidget(userMessage: userMessage!);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
PracticeActivity(
|
||||
practiceEvent: currentActivity!,
|
||||
controller: this,
|
||||
),
|
||||
navigationButtons,
|
||||
],
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 350,
|
||||
minWidth: 350,
|
||||
minHeight: minCardHeight,
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Main content
|
||||
const Positioned(
|
||||
child: PointsGainedAnimation(),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: activityWidget,
|
||||
),
|
||||
// Conditionally show the darkening and progress indicator based on the loading state
|
||||
if (!savoringTheJoy && fetchingActivity) ...[
|
||||
// Semi-transparent overlay
|
||||
Container(
|
||||
color: Colors.black.withOpacity(0.5), // Darkening effect
|
||||
),
|
||||
// Circular progress indicator in the center
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
// Flag button in the top right corner
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: ContentIssueButton(
|
||||
isActive: currentActivity != null,
|
||||
submitFeedback: submitFeedback,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Practice activity content
|
||||
class PracticeActivity extends StatefulWidget {
|
||||
final PracticeActivityEvent practiceEvent;
|
||||
final MessagePracticeActivityCardState controller;
|
||||
|
||||
const PracticeActivity({
|
||||
super.key,
|
||||
required this.practiceEvent,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
PracticeActivityContentState createState() => PracticeActivityContentState();
|
||||
}
|
||||
|
||||
class PracticeActivityContentState extends State<PracticeActivity> {
|
||||
Widget get activityWidget {
|
||||
switch (widget.practiceEvent.practiceActivity.activityType) {
|
||||
case ActivityTypeEnum.multipleChoice:
|
||||
return MultipleChoiceActivity(
|
||||
controller: widget.controller,
|
||||
currentActivity: widget.practiceEvent,
|
||||
);
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
activityWidget,
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.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';
|
||||
|
||||
/// Seperated out the target tokens from the practice activity card
|
||||
/// in order to control the state of the target tokens
|
||||
class TargetTokensController {
|
||||
List<TokenWithXP>? _targetTokens;
|
||||
|
||||
TargetTokensController();
|
||||
|
||||
/// From the tokens in the message, do a preliminary filtering of which to target
|
||||
/// Then get the construct uses for those tokens
|
||||
Future<List<TokenWithXP>> targetTokens(
|
||||
BuildContext context,
|
||||
PangeaMessageEvent pangeaMessageEvent,
|
||||
) async {
|
||||
if (_targetTokens != null) {
|
||||
return _targetTokens!;
|
||||
}
|
||||
|
||||
_targetTokens = await _initialize(context, pangeaMessageEvent);
|
||||
|
||||
await updateTokensWithConstructs(
|
||||
MatrixState.pangeaController.analytics.analyticsStream.value ?? [],
|
||||
context,
|
||||
pangeaMessageEvent,
|
||||
);
|
||||
|
||||
return _targetTokens!;
|
||||
}
|
||||
|
||||
Future<List<TokenWithXP>> _initialize(
|
||||
BuildContext context,
|
||||
PangeaMessageEvent pangeaMessageEvent,
|
||||
) async {
|
||||
if (!context.mounted) {
|
||||
ErrorHandler.logError(
|
||||
m: 'getTargetTokens called when not mounted',
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return _targetTokens = [];
|
||||
}
|
||||
|
||||
final tokens = await pangeaMessageEvent
|
||||
.representationByLanguage(pangeaMessageEvent.messageDisplayLangCode)
|
||||
?.tokensGlobal(context);
|
||||
|
||||
if (tokens == null || tokens.isEmpty) {
|
||||
debugger(when: kDebugMode);
|
||||
return _targetTokens = [];
|
||||
}
|
||||
|
||||
_targetTokens = [];
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
//don't bother with tokens that we don't save to vocab
|
||||
if (!tokens[i].lemma.saveVocab) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_targetTokens!.add(tokens[i].emptyTokenWithXP);
|
||||
}
|
||||
|
||||
return _targetTokens!;
|
||||
}
|
||||
|
||||
Future<void> updateTokensWithConstructs(
|
||||
List<OneConstructUse> constructUses,
|
||||
context,
|
||||
pangeaMessageEvent,
|
||||
) async {
|
||||
final ConstructListModel constructList = ConstructListModel(
|
||||
uses: constructUses,
|
||||
type: null,
|
||||
);
|
||||
|
||||
_targetTokens ??= await _initialize(context, pangeaMessageEvent);
|
||||
|
||||
for (final token in _targetTokens!) {
|
||||
for (final construct in token.constructs) {
|
||||
final constructUseModel = constructList.getConstructUses(
|
||||
construct.id.lemma,
|
||||
construct.id.type,
|
||||
);
|
||||
if (constructUseModel != null) {
|
||||
construct.xp += constructUseModel.points;
|
||||
construct.lastUsed = constructUseModel.lastUsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
lib/pangea/widgets/select_to_define.dart
Normal file
26
lib/pangea/widgets/select_to_define.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:fluffychat/pangea/utils/bot_style.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
class SelectToDefine extends StatelessWidget {
|
||||
const SelectToDefine({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minHeight: minCardHeight),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Center(
|
||||
child: Text(
|
||||
L10n.of(context)!.selectToDefine,
|
||||
style: BotStyle.text(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@ Future<void> pLanguageDialog(
|
|||
future: () async {
|
||||
try {
|
||||
pangeaController.myAnalytics
|
||||
.updateAnalytics()
|
||||
.sendLocalAnalyticsToAnalyticsRoom()
|
||||
.then((_) {
|
||||
pangeaController.userController.updateProfile(
|
||||
(profile) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ description: Learn a language while texting your friends.
|
|||
# Pangea#
|
||||
publish_to: none
|
||||
# On version bump also increase the build number for F-Droid
|
||||
version: 1.21.4+3536
|
||||
version: 1.21.4+3539
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue