Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
4953e63bf8
68 changed files with 2229 additions and 1264 deletions
14
.github/workflows/main_deploy.yaml
vendored
14
.github/workflows/main_deploy.yaml
vendored
|
|
@ -10,15 +10,15 @@ 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
|
||||
|
|
|
|||
|
|
@ -3968,7 +3968,7 @@
|
|||
"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 a word to see its definition!",
|
||||
"translations": "translations",
|
||||
"messageAudio": "message audio",
|
||||
"definitions": "definitions",
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -4505,7 +4505,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",
|
||||
|
|
|
|||
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,6 +15,7 @@ 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';
|
||||
|
|
@ -27,7 +28,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';
|
||||
|
|
@ -654,14 +654,14 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
// stream sends the data for newly sent messages.
|
||||
if (msgEventId != null) {
|
||||
pangeaController.myAnalytics.setState(
|
||||
data: {
|
||||
'eventID': msgEventId,
|
||||
'eventType': EventTypes.Message,
|
||||
'roomID': room.id,
|
||||
'originalSent': originalSent,
|
||||
'tokensSent': tokensSent,
|
||||
'choreo': choreo,
|
||||
},
|
||||
AnalyticsStream(
|
||||
eventId: msgEventId,
|
||||
eventType: EventTypes.Message,
|
||||
roomId: room.id,
|
||||
originalSent: originalSent,
|
||||
tokensSent: tokensSent,
|
||||
choreo: choreo,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1303,8 +1303,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 +1609,6 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
});
|
||||
|
||||
// #Pangea
|
||||
final textSelection = MessageTextSelection();
|
||||
|
||||
void showToolbar(
|
||||
PangeaMessageEvent pangeaMessageEvent, {
|
||||
MessageMode? mode,
|
||||
|
|
@ -1643,10 +1640,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 +1667,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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -239,7 +240,9 @@ class Message extends StatelessWidget {
|
|||
// ),
|
||||
// )
|
||||
// else if (nextEventSameSender || ownMessage)
|
||||
if (nextEventSameSender || ownMessage || isOverlay)
|
||||
if (nextEventSameSender ||
|
||||
ownMessage ||
|
||||
overlayController != null)
|
||||
// Pangea#
|
||||
SizedBox(
|
||||
width: Avatar.defaultSize,
|
||||
|
|
@ -281,7 +284,8 @@ class Message extends StatelessWidget {
|
|||
children: [
|
||||
// #Pangea
|
||||
// if (!nextEventSameSender)
|
||||
if (!nextEventSameSender && !isOverlay)
|
||||
if (!nextEventSameSender &&
|
||||
overlayController == null)
|
||||
// Pangea#
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
|
|
@ -348,14 +352,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
|
||||
|
|
@ -448,7 +452,8 @@ class Message extends StatelessWidget {
|
|||
pangeaMessageEvent:
|
||||
pangeaMessageEvent,
|
||||
immersionMode: immersionMode,
|
||||
isOverlay: isOverlay,
|
||||
overlayController:
|
||||
overlayController,
|
||||
controller: controller,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: previousEvent,
|
||||
|
|
@ -536,7 +541,7 @@ class Message extends StatelessWidget {
|
|||
event.hasAggregatedEvents(timeline, RelationshipTypes.reaction);
|
||||
// #Pangea
|
||||
// if (showReceiptsRow || displayTime || selected || displayReadMarker) {
|
||||
if (!isOverlay &&
|
||||
if (overlayController == null &&
|
||||
(showReceiptsRow ||
|
||||
displayTime ||
|
||||
displayReadMarker ||
|
||||
|
|
@ -577,7 +582,7 @@ class Message extends StatelessWidget {
|
|||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
// #Pangea
|
||||
child: isOverlay ||
|
||||
child: overlayController != null ||
|
||||
(!showReceiptsRow &&
|
||||
!(pangeaMessageEvent?.showMessageButtons ?? false))
|
||||
// child: !showReceiptsRow
|
||||
|
|
@ -670,7 +675,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_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_toolbar.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,28 @@ 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) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "reconstructed text does not match current text",
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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,20 +66,22 @@ class ChoicesArrayState extends State<ChoicesArray> {
|
|||
? ItShimmer(originalSpan: widget.originalSpan)
|
||||
: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: widget.choices
|
||||
?.asMap()
|
||||
.entries
|
||||
.map(
|
||||
(entry) => ChoiceItem(
|
||||
children: widget.choices!
|
||||
.mapIndexed(
|
||||
(index, entry) => ChoiceItem(
|
||||
theme: theme,
|
||||
onLongPress:
|
||||
widget.isActive ? widget.onLongPress : null,
|
||||
onPressed: widget.isActive ? widget.onPressed : (_) {},
|
||||
entry: entry,
|
||||
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 == entry.key,
|
||||
isSelected: widget.selectedChoiceIndex == index,
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
|
|
@ -112,8 +117,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 +141,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 +173,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),
|
||||
|
|
|
|||
|
|
@ -393,8 +393,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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
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 +41,9 @@ 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();
|
||||
int get level => currentXP ~/ 10;
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,11 +25,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>? _messageSendSubscription;
|
||||
Timer? _updateTimer;
|
||||
|
||||
Client get _client => _pangeaController.matrixState.client;
|
||||
|
|
@ -48,7 +48,7 @@ class MyAnalyticsController extends BaseController {
|
|||
final int _maxMessagesCached = 10;
|
||||
|
||||
/// the number of minutes before an automatic update is triggered
|
||||
final int _minutesBeforeUpdate = 5;
|
||||
final int _minutesBeforeUpdate = 2;
|
||||
|
||||
/// the time since the last update that will trigger an automatic update
|
||||
final Duration _timeSinceUpdate = const Duration(days: 1);
|
||||
|
|
@ -60,9 +60,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>));
|
||||
_messageSendSubscription ??=
|
||||
stateStream.listen((data) => _onNewAnalyticsData(data));
|
||||
|
||||
_refreshAnalyticsIfOutdated();
|
||||
}
|
||||
|
|
@ -103,77 +102,59 @@ 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();
|
||||
});
|
||||
|
||||
// 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'];
|
||||
|
||||
if (roomID == null || eventID == null) return;
|
||||
|
||||
void _onNewAnalyticsData(AnalyticsStream data) {
|
||||
// convert that data into construct uses and add it to the cache
|
||||
final metadata = ConstructUseMetaData(
|
||||
roomId: roomID,
|
||||
eventId: eventID,
|
||||
roomId: data.roomId,
|
||||
eventId: data.eventId,
|
||||
timeStamp: DateTime.now(),
|
||||
);
|
||||
|
||||
final List<OneConstructUse> constructs = getDraftUses(roomID);
|
||||
final List<OneConstructUse> constructs = _getDraftUses(data.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;
|
||||
if (data.eventType == EventTypes.Message) {
|
||||
constructs.addAll([
|
||||
...(grammarConstructs ?? []),
|
||||
...(vocabUses ?? []),
|
||||
...(data.choreo!.grammarConstructUses(metadata: metadata)),
|
||||
...(data.originalSent!.vocabUses(
|
||||
choreo: data.choreo,
|
||||
tokens: data.tokensSent!.tokens,
|
||||
metadata: metadata,
|
||||
)),
|
||||
]);
|
||||
}
|
||||
|
||||
if (eventType == PangeaEventTypes.activityRecord &&
|
||||
practiceActivity != null) {
|
||||
final activityConstructs = recordModel?.uses(
|
||||
practiceActivity,
|
||||
} else if (data.eventType == PangeaEventTypes.activityRecord &&
|
||||
data.practiceActivity != null) {
|
||||
final activityConstructs = data.recordModel!.uses(
|
||||
data.practiceActivity!,
|
||||
metadata: metadata,
|
||||
);
|
||||
constructs.addAll(activityConstructs ?? []);
|
||||
constructs.addAll(activityConstructs);
|
||||
} else {
|
||||
throw PangeaWarningError("Invalid event type for analytics stream");
|
||||
}
|
||||
|
||||
final String eventID = data.eventId;
|
||||
final String roomID = data.roomId;
|
||||
|
||||
_pangeaController.analytics
|
||||
.filterConstructs(unfilteredConstructs: constructs)
|
||||
.then((filtered) {
|
||||
if (filtered.isEmpty) return;
|
||||
filtered.addAll(getDraftUses(roomID));
|
||||
|
||||
// @ggurdin - are we sure this isn't happening twice? it's also above
|
||||
filtered.addAll(_getDraftUses(data.roomId));
|
||||
|
||||
final level = _pangeaController.analytics.level;
|
||||
addLocalMessage(eventID, filtered).then(
|
||||
|
||||
_addLocalMessage(eventID, filtered).then(
|
||||
(_) {
|
||||
clearDraftUses(roomID);
|
||||
afterAddLocalMessages(level);
|
||||
_clearDraftUses(roomID);
|
||||
_decideWhetherToUpdateAnalyticsRoom(level);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -216,26 +197,28 @@ class MyAnalyticsController extends BaseController {
|
|||
}
|
||||
}
|
||||
|
||||
// @ggurdin - if the point of draft uses is that we don't want to send them twice,
|
||||
// then, if this is triggered here, couldn't that make a problem?
|
||||
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(
|
||||
Future<void> _addLocalMessage(
|
||||
String eventID,
|
||||
List<OneConstructUse> constructs,
|
||||
) async {
|
||||
|
|
@ -244,7 +227,7 @@ class MyAnalyticsController extends BaseController {
|
|||
constructs.addAll(currentCache[eventID] ?? []);
|
||||
currentCache[eventID] = constructs;
|
||||
|
||||
await setMessagesSinceUpdate(currentCache);
|
||||
await _setMessagesSinceUpdate(currentCache);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: PangeaWarningError("Failed to add message since update: $e"),
|
||||
|
|
@ -258,17 +241,25 @@ 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);
|
||||
}
|
||||
|
||||
|
|
@ -278,7 +269,7 @@ class MyAnalyticsController extends BaseController {
|
|||
}
|
||||
|
||||
/// 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 +293,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 +339,46 @@ class MyAnalyticsController extends BaseController {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsStream {
|
||||
final String eventId;
|
||||
final String eventType;
|
||||
final String roomId;
|
||||
|
||||
/// if the event is a message, the original message sent
|
||||
final PangeaRepresentation? originalSent;
|
||||
|
||||
/// if the event is a message, the tokens sent
|
||||
final PangeaMessageTokens? tokensSent;
|
||||
|
||||
/// if the event is a message, the choreo record
|
||||
final ChoreoRecord? choreo;
|
||||
|
||||
/// if the event is a practice activity, the practice activity event
|
||||
final PracticeActivityEvent? practiceActivity;
|
||||
|
||||
/// if the event is a practice activity, the record model
|
||||
final PracticeActivityRecordModel? recordModel;
|
||||
|
||||
AnalyticsStream({
|
||||
required this.eventId,
|
||||
required this.eventType,
|
||||
required this.roomId,
|
||||
this.originalSent,
|
||||
this.tokensSent,
|
||||
this.choreo,
|
||||
this.practiceActivity,
|
||||
this.recordModel,
|
||||
}) {
|
||||
assert(
|
||||
(originalSent != null && tokensSent != null && choreo != null) ||
|
||||
(practiceActivity != null && recordModel != null),
|
||||
"Either a message or a practice activity must be provided",
|
||||
);
|
||||
|
||||
assert(
|
||||
eventType == EventTypes.Message ||
|
||||
eventType == PangeaEventTypes.activityRecord,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,17 @@ 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 (res.activity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Future<PracticeActivityEvent?> eventFuture =
|
||||
_sendAndPackageEvent(dummyModel(event), event);
|
||||
_sendAndPackageEvent(res.activity!, event);
|
||||
|
||||
_cache[cacheKey] =
|
||||
_RequestCacheItem(req: req, practiceActivityEvent: eventFuture);
|
||||
|
|
@ -85,7 +133,7 @@ class PracticeGenerationController {
|
|||
}
|
||||
}
|
||||
|
||||
PracticeActivityModel dummyModel(PangeaMessageEvent event) =>
|
||||
PracticeActivityModel _dummyModel(PangeaMessageEvent event) =>
|
||||
PracticeActivityModel(
|
||||
tgtConstructs: [
|
||||
ConstructIdentifier(lemma: "be", type: ConstructTypeEnum.vocab),
|
||||
|
|
@ -97,6 +145,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
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class WordController extends BaseController {
|
|||
if (local == null) {
|
||||
if (_wordData.length > 100) _wordData.clear();
|
||||
_wordData.add(w);
|
||||
setState();
|
||||
setState(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]!;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:collection/collection.dart';
|
|||
import 'package:fluffychat/pangea/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
|
|
@ -15,7 +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/utils/bot_name.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';
|
||||
|
|
@ -351,6 +350,7 @@ class PangeaMessageEvent {
|
|||
_representations?.add(
|
||||
RepresentationEvent(
|
||||
timeline: timeline,
|
||||
parentMessageEvent: _event,
|
||||
content: PangeaRepresentation(
|
||||
langCode: response.langCode,
|
||||
text: response.transcript.text,
|
||||
|
|
@ -364,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) {
|
||||
|
|
@ -415,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,
|
||||
),
|
||||
);
|
||||
|
|
@ -444,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) {
|
||||
|
|
@ -489,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
|
||||
|
|
@ -547,17 +559,20 @@ class PangeaMessageEvent {
|
|||
}
|
||||
}
|
||||
|
||||
bool get showUseType =>
|
||||
!ownMessage &&
|
||||
_event.room.isSpaceAdmin &&
|
||||
_event.senderId != BotName.byEnvironment &&
|
||||
!room.isUserSpaceAdmin(_event.senderId) &&
|
||||
_event.messageType != PangeaEventTypes.report &&
|
||||
_event.messageType == MessageTypes.Text;
|
||||
bool get showUseType => false;
|
||||
// *note* turning this feature off but leave code here to bring back (if need)
|
||||
// !ownMessage &&
|
||||
// _event.room.isSpaceAdmin &&
|
||||
// _event.senderId != BotName.byEnvironment &&
|
||||
// !room.isUserSpaceAdmin(_event.senderId) &&
|
||||
// _event.messageType != PangeaEventTypes.report &&
|
||||
// _event.messageType == MessageTypes.Text;
|
||||
|
||||
// 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.
|
||||
///
|
||||
|
|
@ -573,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
|
||||
|
|
@ -588,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;
|
||||
|
|
@ -637,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,
|
||||
|
|
@ -102,23 +103,23 @@ class RepresentationEvent {
|
|||
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 +130,7 @@ class RepresentationEvent {
|
|||
),
|
||||
);
|
||||
|
||||
if (tokensEvent == null) return null;
|
||||
|
||||
_tokens = TokensEvent(event: tokensEvent).tokens;
|
||||
|
||||
return _tokens?.tokens;
|
||||
return res;
|
||||
}
|
||||
|
||||
ChoreoRecord? get choreo {
|
||||
|
|
|
|||
|
|
@ -35,8 +35,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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
/// 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 + value.first.constructType.string,
|
||||
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,7 +76,7 @@ class OneConstructUse {
|
|||
String? lemma;
|
||||
String? form;
|
||||
List<String> categories;
|
||||
ConstructTypeEnum? constructType;
|
||||
ConstructTypeEnum constructType;
|
||||
ConstructUseTypeEnum useType;
|
||||
String? id;
|
||||
ConstructUseMetaData metadata;
|
||||
|
|
@ -96,6 +96,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 +110,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 +120,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 +128,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
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ class ChoreoRecord {
|
|||
lemma: name,
|
||||
form: name,
|
||||
constructType: ConstructTypeEnum.grammar,
|
||||
// @ggurdin what is this used for?
|
||||
id: "${metadata.eventId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}",
|
||||
metadata: metadata,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -126,14 +127,47 @@ 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++) {
|
||||
final PangeaToken token = tokens[i];
|
||||
token.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) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "reconstructed text does not match original input",
|
||||
);
|
||||
}
|
||||
|
||||
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 +176,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 +189,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,43 @@ class PangeaToken {
|
|||
required this.morph,
|
||||
});
|
||||
|
||||
static String reconstructText(
|
||||
List<PangeaToken> tokens, [
|
||||
int startTokenIndex = 0,
|
||||
int endTokenIndex = -1,
|
||||
]) {
|
||||
if (endTokenIndex == -1) {
|
||||
endTokenIndex = tokens.length - 1;
|
||||
}
|
||||
|
||||
final List<PangeaToken> subset =
|
||||
tokens.whereIndexed((int index, PangeaToken token) {
|
||||
return index >= startTokenIndex && index <= endTokenIndex;
|
||||
}).toList();
|
||||
|
||||
if (subset.isEmpty) {
|
||||
debugger(when: kDebugMode);
|
||||
return '';
|
||||
}
|
||||
|
||||
if (subset.length == 1) {
|
||||
return subset.first.text.content;
|
||||
}
|
||||
|
||||
String reconstruction = subset.first.text.content;
|
||||
for (int i = 1; i < subset.length - 1; i++) {
|
||||
int whitespace = subset[i].text.offset -
|
||||
(subset[i - 1].text.offset + subset[i - 1].text.length);
|
||||
if (whitespace < 0) {
|
||||
debugger(when: kDebugMode);
|
||||
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 +108,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 +175,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,178 @@
|
|||
import 'package:collection/collection.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;
|
||||
final 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() {
|
||||
return {
|
||||
'construct_id': id.toJson(),
|
||||
'xp': xp,
|
||||
'last_used': lastUsed?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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 MessageActivityRequest {
|
||||
final String userL1;
|
||||
final String userL2;
|
||||
|
||||
final String messageText;
|
||||
final List<TokenWithXP> tokensWithXP;
|
||||
|
||||
final String messageId;
|
||||
|
||||
MessageActivityRequest({
|
||||
required this.userL1,
|
||||
required this.userL2,
|
||||
required this.messageText,
|
||||
required this.tokensWithXP,
|
||||
required this.messageId,
|
||||
});
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
MessageActivityResponse({
|
||||
required this.activity,
|
||||
required this.finished,
|
||||
});
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'activity': activity?.toJson(),
|
||||
'finished': finished,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
@ -269,6 +284,8 @@ class PracticeActivityModel {
|
|||
);
|
||||
}
|
||||
|
||||
RelevantSpanDisplayDetails? get relevantSpanDisplayDetails =>
|
||||
multipleChoice?.spanDisplayDetails;
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'tgt_constructs': tgtConstructs.map((e) => e.toJson()).toList(),
|
||||
|
|
@ -282,20 +299,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 +361,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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ class PracticeActivityRecordModel {
|
|||
: ConstructUseTypeEnum.incPA)
|
||||
: ConstructUseTypeEnum.unk;
|
||||
|
||||
bool hasTextResponse(String text) {
|
||||
return responses.any((element) => element.text == text);
|
||||
}
|
||||
|
||||
void addResponse({
|
||||
String? text,
|
||||
Uint8List? audioBytes,
|
||||
|
|
@ -80,7 +84,7 @@ class PracticeActivityRecordModel {
|
|||
),
|
||||
);
|
||||
} catch (e) {
|
||||
debugger();
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>(),
|
||||
|
|
|
|||
|
|
@ -54,6 +54,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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,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 +56,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':
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
|
|
@ -5,44 +7,59 @@ 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/overlay_footer.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/overlay_header.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: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
|
||||
int needed = 3;
|
||||
|
||||
/// Whether the user has completed the activities needed to unlock the toolbar
|
||||
/// within this overlay 'session'. if they click out and come back in then
|
||||
/// we can give them some more activities to complete
|
||||
bool finishedActivitiesThisSession = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -50,8 +67,119 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
vsync: this,
|
||||
duration: FluffyThemes.animationDuration,
|
||||
);
|
||||
|
||||
setInitialToolbarMode();
|
||||
}
|
||||
|
||||
int get activitiesLeftToComplete =>
|
||||
needed - widget._pangeaMessageEvent.numberOfActivitiesCompleted;
|
||||
|
||||
bool get isPracticeComplete => activitiesLeftToComplete <= 0;
|
||||
|
||||
/// In some cases, we need to exit the practice flow and let the user
|
||||
/// interact with the toolbar without completing activities
|
||||
void exitPracticeFlow() {
|
||||
needed = 0;
|
||||
setInitialToolbarMode();
|
||||
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 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 onNewActivity(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;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
|
@ -108,8 +236,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 +251,7 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
}
|
||||
|
||||
RenderBox? get messageRenderBox => MatrixState.pAnyState.getRenderBox(
|
||||
widget.event.eventId,
|
||||
widget._event.eventId,
|
||||
);
|
||||
|
||||
Size? get messageSize => messageRenderBox?.size;
|
||||
|
|
@ -146,7 +274,7 @@ 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(
|
||||
constraints: const BoxConstraints(
|
||||
|
|
@ -158,40 +286,38 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: widget.pangeaMessageEvent.ownMessage
|
||||
mainAxisAlignment: widget._pangeaMessageEvent.ownMessage
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: widget.pangeaMessageEvent.ownMessage
|
||||
left: widget._pangeaMessageEvent.ownMessage
|
||||
? 0
|
||||
: Avatar.defaultSize + 16,
|
||||
right: widget.pangeaMessageEvent.ownMessage ? 8 : 0,
|
||||
right: widget._pangeaMessageEvent.ownMessage ? 8 : 0,
|
||||
),
|
||||
child: MessageToolbar(
|
||||
pangeaMessageEvent: widget.pangeaMessageEvent,
|
||||
controller: widget.controller,
|
||||
textSelection: widget.textSelection,
|
||||
initialMode: widget.initialMode,
|
||||
pangeaMessageEvent: widget._pangeaMessageEvent,
|
||||
overLayController: this,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Message(
|
||||
widget.event,
|
||||
widget._event,
|
||||
onSwipe: () => {},
|
||||
onInfoTab: (_) => {},
|
||||
onAvatarTab: (_) => {},
|
||||
scrollToEventId: (_) => {},
|
||||
onSelect: (_) => {},
|
||||
immersionMode: widget.controller.choreographer.immersionMode,
|
||||
controller: widget.controller,
|
||||
timeline: widget.controller.timeline!,
|
||||
isOverlay: true,
|
||||
immersionMode: widget.chatController.choreographer.immersionMode,
|
||||
controller: widget.chatController,
|
||||
timeline: widget.chatController.timeline!,
|
||||
overlayController: this,
|
||||
animateIn: false,
|
||||
nextEvent: widget.nextEvent,
|
||||
previousEvent: widget.prevEvent,
|
||||
nextEvent: widget._nextEvent,
|
||||
previousEvent: widget._prevEvent,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -240,7 +366,7 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
OverlayFooter(controller: widget.controller),
|
||||
OverlayFooter(controller: widget.chatController),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -252,7 +378,7 @@ class MessageSelectionOverlayState extends State<MessageSelectionOverlay>
|
|||
),
|
||||
),
|
||||
Material(
|
||||
child: OverlayHeader(controller: widget.controller),
|
||||
child: OverlayHeader(controller: widget.chatController),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,5 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pages/chat/chat.dart';
|
||||
|
|
@ -7,29 +9,25 @@ 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/widgets/matrix.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
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,203 +35,113 @@ 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;
|
||||
}
|
||||
// why can't this just be initstate or the build mode?
|
||||
// WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
// //determine the starting mode
|
||||
// // if (widget.pangeaMessageEvent.isAudioMessage) {
|
||||
// // updateMode(MessageMode.speechToText);
|
||||
// // return;
|
||||
// // }
|
||||
|
||||
if (widget.initialMode != null) {
|
||||
updateMode(widget.initialMode!);
|
||||
} else {
|
||||
MatrixState.pangeaController.userController.profile.userSettings
|
||||
.autoPlayMessages
|
||||
? updateMode(MessageMode.textToSpeech)
|
||||
: updateMode(MessageMode.translation);
|
||||
}
|
||||
});
|
||||
// // if (widget.initialMode != null) {
|
||||
// // updateMode(widget.initialMode!);
|
||||
// // } else {
|
||||
// // MatrixState.pangeaController.userController.profile.userSettings
|
||||
// // .autoPlayMessages
|
||||
// // ? updateMode(MessageMode.textToSpeech)
|
||||
// // : updateMode(MessageMode.translation);
|
||||
// // }
|
||||
// // });
|
||||
|
||||
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!);
|
||||
// // just set mode based on messageSelectionOverlay mode which is now handling the state
|
||||
// updateMode(widget.overLayController.toolbarMode);
|
||||
// });
|
||||
}
|
||||
|
||||
Widget get toolbarContent {
|
||||
final bool subscribed =
|
||||
MatrixState.pangeaController.subscriptionController.isSubscribed;
|
||||
|
||||
if (!subscribed) {
|
||||
return MessageUnsubscribedCard(
|
||||
languageTool: widget.overLayController.toolbarMode.title(context),
|
||||
mode: widget.overLayController.toolbarMode,
|
||||
controller: this,
|
||||
);
|
||||
}
|
||||
|
||||
switch (widget.overLayController.toolbarMode) {
|
||||
case MessageMode.translation:
|
||||
return MessageTranslationCard(
|
||||
messageEvent: widget.pangeaMessageEvent,
|
||||
selection: widget.overLayController.selectedSpan,
|
||||
);
|
||||
case MessageMode.textToSpeech:
|
||||
return MessageAudioCard(
|
||||
messageEvent: widget.pangeaMessageEvent,
|
||||
);
|
||||
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(),
|
||||
);
|
||||
|
||||
debugPrint("building toolbar");
|
||||
return Material(
|
||||
key: MatrixState.pAnyState
|
||||
.layerLinkAndKey('${widget.pangeaMessageEvent.eventId}-toolbar')
|
||||
|
|
@ -245,7 +153,7 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
maxWidth: 275,
|
||||
minWidth: 275,
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border.all(
|
||||
|
|
@ -259,16 +167,15 @@ class MessageToolbarState extends State<MessageToolbar> {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (toolbarContent != null)
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: toolbarContent,
|
||||
),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
child: toolbarContent,
|
||||
),
|
||||
),
|
||||
buttonRow,
|
||||
),
|
||||
ToolbarButtons(messageToolbarController: this, width: 250),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -296,30 +203,144 @@ class ToolbarSelectionArea extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SelectionArea(
|
||||
onSelectionChanged: (SelectedContent? selection) {
|
||||
controller.textSelection.onSelection(selection?.plainText);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (pangeaMessageEvent != null && !isOverlay) {
|
||||
controller.showToolbar(
|
||||
pangeaMessageEvent!,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
);
|
||||
}
|
||||
},
|
||||
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,
|
||||
onLongPress: () {
|
||||
if (pangeaMessageEvent != null && !isOverlay) {
|
||||
controller.showToolbar(
|
||||
pangeaMessageEvent!,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarButtons extends StatefulWidget {
|
||||
final MessageToolbarState messageToolbarController;
|
||||
final double width;
|
||||
|
||||
const ToolbarButtons({
|
||||
required this.messageToolbarController,
|
||||
required this.width,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ToolbarButtonsState createState() => ToolbarButtonsState();
|
||||
}
|
||||
|
||||
class ToolbarButtonsState extends State<ToolbarButtons> {
|
||||
PangeaMessageEvent get pangeaMessageEvent =>
|
||||
widget.messageToolbarController.widget.pangeaMessageEvent;
|
||||
|
||||
List<MessageMode> get modes => MessageMode.values
|
||||
.where((mode) => mode.isValidMode(pangeaMessageEvent.event))
|
||||
.toList();
|
||||
|
||||
static const double iconWidth = 36.0;
|
||||
double get progressWidth => widget.width / overlayController.needed;
|
||||
|
||||
MessageOverlayController get overlayController =>
|
||||
widget.messageToolbarController.widget.overLayController;
|
||||
|
||||
// @ggurdin - maybe this can be stateless now?
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget
|
||||
.messageToolbarController.widget.pangeaMessageEvent.isAudioMessage) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: widget.width,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: MessageModeExtension.barAndLockedButtonColor(context),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: iconWidth / 2),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
height: 12,
|
||||
width: min(
|
||||
widget.width,
|
||||
progressWidth *
|
||||
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 ==
|
||||
widget.messageToolbarController.widget
|
||||
.overLayController.toolbarMode
|
||||
? Colors.white
|
||||
: null,
|
||||
isSelected: mode ==
|
||||
widget.messageToolbarController.widget
|
||||
.overLayController.toolbarMode,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
mode.iconButtonColor(
|
||||
context,
|
||||
index,
|
||||
widget.messageToolbarController.widget
|
||||
.overLayController.toolbarMode,
|
||||
pangeaMessageEvent.numberOfActivitiesCompleted,
|
||||
widget.messageToolbarController.widget
|
||||
.overLayController.isPracticeComplete,
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: mode.isUnlocked(
|
||||
index,
|
||||
pangeaMessageEvent.numberOfActivitiesCompleted,
|
||||
overlayController.isPracticeComplete,
|
||||
)
|
||||
? () => widget
|
||||
.messageToolbarController.widget.overLayController
|
||||
.updateToolbarMode(mode)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
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/toolbar_content_loading_indicator.dart';
|
||||
import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -13,13 +13,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 +28,27 @@ 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() {
|
||||
print('MessageTranslationCard initState');
|
||||
super.initState();
|
||||
loadTranslation();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MessageTranslationCard oldWidget) {
|
||||
// debugger(when: kDebugMode);
|
||||
if (oldWidget.selection != widget.selection) {
|
||||
debugPrint('selection changed');
|
||||
loadTranslation();
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
Future<void> fetchRepresentationText() async {
|
||||
// debugger(when: kDebugMode);
|
||||
if (l1Code == null) return;
|
||||
|
||||
repEvent = widget.messageEvent
|
||||
|
|
@ -49,48 +64,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,27 +114,6 @@ 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(),
|
||||
|
|
@ -144,23 +138,24 @@ class MessageTranslationCardState extends State<MessageTranslationCard> {
|
|||
final bool isTextIdentical = selectionTranslation != null &&
|
||||
widget.messageEvent.originalSent?.text == selectionTranslation;
|
||||
|
||||
return isWrittenInL1 || isTextIdentical;
|
||||
return (isWrittenInL1 || isTextIdentical) && widget.messageEvent.ownMessage;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_fetchingRepresentation &&
|
||||
print('MessageTranslationCard build');
|
||||
if (!_fetchingTranslation &&
|
||||
repEvent == null &&
|
||||
selectionTranslation == null) {
|
||||
return const CardErrorWidget();
|
||||
}
|
||||
|
||||
return Container(
|
||||
child: _fetchingRepresentation
|
||||
child: _fetchingTranslation
|
||||
? const ToolbarContentLoadingIndicator()
|
||||
: Column(
|
||||
children: [
|
||||
selectionTranslation != null
|
||||
widget.selection != null
|
||||
? Text(
|
||||
selectionTranslation!,
|
||||
style: BotStyle.text(context),
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class MessageUnsubscribedCard extends StatelessWidget {
|
|||
if (inTrialWindow) {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.activateNewUserTrial();
|
||||
controller.updateMode(mode);
|
||||
controller.widget.overLayController.updateToolbarMode(mode);
|
||||
} else {
|
||||
MatrixState.pangeaController.subscriptionController
|
||||
.showPaywall(context);
|
||||
|
|
|
|||
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 = () {
|
||||
print(
|
||||
'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,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),
|
||||
|
|
|
|||
|
|
@ -13,6 +13,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';
|
||||
|
|
@ -142,7 +143,9 @@ 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,
|
||||
),
|
||||
LevelBarDetails(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}),
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ class WordDataCardController extends State<WordDataCard> {
|
|||
|
||||
@override
|
||||
void didUpdateWidget(covariant WordDataCard oldWidget) {
|
||||
// debugger(when: kDebugMode);
|
||||
if (oldWidget.word != widget.word) {
|
||||
if (!widget.hasInfo) {
|
||||
getContextualDefinition();
|
||||
|
|
|
|||
|
|
@ -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,5 +1,6 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
|
||||
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/practice_activities.dart/practice_activity_model.dart';
|
||||
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
|
||||
|
|
@ -8,12 +9,12 @@ 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,7 +26,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
int? selectedChoiceIndex;
|
||||
|
||||
PracticeActivityRecordModel? get currentRecordModel =>
|
||||
widget.controller.currentRecordModel;
|
||||
widget.practiceCardController.currentCompletionRecord;
|
||||
|
||||
bool get isSubmitted =>
|
||||
widget.currentActivity?.userRecord?.record.latestResponse != null;
|
||||
|
|
@ -52,7 +53,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
/// determines the selected choice index.
|
||||
void setCompletionRecord() {
|
||||
if (widget.currentActivity?.userRecord?.record == null) {
|
||||
widget.controller.setCurrentModel(
|
||||
widget.practiceCardController.setCompletionRecord(
|
||||
PracticeActivityRecordModel(
|
||||
question:
|
||||
widget.currentActivity?.practiceActivity.multipleChoice!.question,
|
||||
|
|
@ -60,8 +61,8 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
);
|
||||
selectedChoiceIndex = null;
|
||||
} else {
|
||||
widget.controller
|
||||
.setCurrentModel(widget.currentActivity!.userRecord!.record);
|
||||
widget.practiceCardController
|
||||
.setCompletionRecord(widget.currentActivity!.userRecord!.record);
|
||||
selectedChoiceIndex = widget
|
||||
.currentActivity?.practiceActivity.multipleChoice!
|
||||
.choiceIndex(currentRecordModel!.latestResponse!.text!);
|
||||
|
|
@ -69,16 +70,41 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
void updateChoice(int index) {
|
||||
void updateChoice(String value, int index) {
|
||||
if (currentRecordModel?.hasTextResponse(value) ?? false) {
|
||||
return;
|
||||
}
|
||||
|
||||
final bool isCorrect = widget
|
||||
.currentActivity!.practiceActivity.multipleChoice!
|
||||
.isCorrect(value, index);
|
||||
|
||||
final ConstructUseTypeEnum useType =
|
||||
isCorrect ? ConstructUseTypeEnum.corPA : ConstructUseTypeEnum.incPA;
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
// TODO - add draft uses
|
||||
// activities currently pass around tgtConstructs but not the token
|
||||
// either we change addDraftUses to take constructs or we get and pass the token
|
||||
// MatrixState.pangeaController.myAnalytics.addDraftUses(
|
||||
// widget.currentActivity.practiceActivity.tg,
|
||||
// widget.practiceCardController.widget.pangeaMessageEvent.room.id,
|
||||
// useType,
|
||||
// );
|
||||
|
||||
// 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,10 +138,11 @@ 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(),
|
||||
|
|
|
|||
|
|
@ -1,25 +1,39 @@
|
|||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
|
||||
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/construct_list_model.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: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/practice_activity/multiple_choice_activity.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,8 +43,10 @@ class PracticeActivityCard extends StatefulWidget {
|
|||
|
||||
class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
||||
PracticeActivityEvent? currentActivity;
|
||||
PracticeActivityRecordModel? currentRecordModel;
|
||||
bool sending = false;
|
||||
PracticeActivityRecordModel? currentCompletionRecord;
|
||||
bool fetchingActivity = false;
|
||||
|
||||
List<TokenWithXP> targetTokens = [];
|
||||
|
||||
List<PracticeActivityEvent> get practiceActivities =>
|
||||
widget.pangeaMessageEvent.practiceActivities;
|
||||
|
|
@ -39,148 +55,332 @@ class MessagePracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
(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;
|
||||
/// TODO - @ggurdin - how can we start our processes (saving results and getting an activity)
|
||||
/// immediately after a correct choice but wait to display until x milliseconds after the choice is made AND
|
||||
/// we've received the new activity?
|
||||
Duration appropriateTimeForJoy = const Duration(milliseconds: 500);
|
||||
bool savoringTheJoy = false;
|
||||
Timer? joyTimer;
|
||||
|
||||
@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;
|
||||
setState(() => fetchingActivity = value);
|
||||
}
|
||||
|
||||
/// Get an activity to display.
|
||||
/// Show an uncompleted activity if there is one.
|
||||
/// If not, get a new activity from the server.
|
||||
Future<void> initialize() async {
|
||||
targetTokens = await getTargetTokens();
|
||||
|
||||
currentActivity = _fetchExistingActivity() ?? await _fetchNewActivity();
|
||||
|
||||
currentActivity == null
|
||||
? widget.overlayController.exitPracticeFlow()
|
||||
: widget.overlayController
|
||||
.onNewActivity(currentActivity!.practiceActivity);
|
||||
}
|
||||
|
||||
// TODO - do more of a check for whether we have an appropropriate activity
|
||||
// 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? _fetchExistingActivity() {
|
||||
final List<PracticeActivityEvent> incompleteActivities =
|
||||
practiceActivities.where((element) => !element.isComplete).toList();
|
||||
currentActivity ??= incompleteActivities.isNotEmpty
|
||||
? incompleteActivities.first
|
||||
: practiceActivities.first;
|
||||
setState(() {});
|
||||
|
||||
final PracticeActivityEvent? existingActivity =
|
||||
incompleteActivities.isNotEmpty ? incompleteActivities.first : null;
|
||||
|
||||
return existingActivity != null &&
|
||||
existingActivity.practiceActivity !=
|
||||
currentActivity?.practiceActivity
|
||||
? existingActivity
|
||||
: null;
|
||||
}
|
||||
|
||||
void setCurrentModel(PracticeActivityRecordModel? recordModel) {
|
||||
currentRecordModel = recordModel;
|
||||
}
|
||||
Future<PracticeActivityEvent?> _fetchNewActivity() async {
|
||||
updateFetchingActivity(true);
|
||||
|
||||
/// 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(() {});
|
||||
if (targetTokens.isEmpty ||
|
||||
!pangeaController.languageController.languagesSet) {
|
||||
debugger(when: kDebugMode);
|
||||
updateFetchingActivity(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
final ourNewActivity =
|
||||
await pangeaController.practiceGenerationController.getPracticeActivity(
|
||||
MessageActivityRequest(
|
||||
userL1: pangeaController.languageController.userL1!.langCode,
|
||||
userL2: pangeaController.languageController.userL2!.langCode,
|
||||
messageText: representation!.text,
|
||||
tokensWithXP: targetTokens,
|
||||
messageId: widget.pangeaMessageEvent.eventId,
|
||||
),
|
||||
widget.pangeaMessageEvent,
|
||||
);
|
||||
|
||||
/// Removes the target tokens of the new activity from the target tokens list.
|
||||
/// This avoids getting activities for the same token again, at least
|
||||
/// until the user exists the toolbar and re-enters it. By then, the
|
||||
/// analytics stream will have updated and the user will be able to get
|
||||
/// activity data for previously targeted tokens. This should then exclude
|
||||
/// the tokens that were targeted in previous activities based on xp and lastUsed.
|
||||
if (ourNewActivity?.practiceActivity.relevantSpanDisplayDetails != null) {
|
||||
targetTokens.removeWhere((token) {
|
||||
final RelevantSpanDisplayDetails span =
|
||||
ourNewActivity!.practiceActivity.relevantSpanDisplayDetails!;
|
||||
return token.token.text.offset >= span.offset &&
|
||||
token.token.text.offset + token.token.text.length <=
|
||||
span.offset + span.length;
|
||||
});
|
||||
}
|
||||
|
||||
updateFetchingActivity(false);
|
||||
|
||||
return ourNewActivity;
|
||||
}
|
||||
|
||||
/// 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>> getTargetTokens() async {
|
||||
if (!mounted) {
|
||||
ErrorHandler.logError(
|
||||
m: 'getTargetTokens called when not mounted',
|
||||
s: StackTrace.current,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// we're just going to set this once per session
|
||||
// we remove the target tokens when we get a new activity
|
||||
if (targetTokens.isNotEmpty) return targetTokens;
|
||||
|
||||
if (representation == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
}
|
||||
final tokens = await representation?.tokensGlobal(context);
|
||||
|
||||
if (tokens == null || tokens.isEmpty) {
|
||||
debugger(when: kDebugMode);
|
||||
return [];
|
||||
}
|
||||
|
||||
var constructUses =
|
||||
MatrixState.pangeaController.analytics.analyticsStream.value;
|
||||
|
||||
if (constructUses == null || constructUses.isEmpty) {
|
||||
constructUses = [];
|
||||
//@gurdin - this is happening for me with a brand-new user. however, in this case, constructUses should be empty list
|
||||
debugger(when: kDebugMode);
|
||||
}
|
||||
|
||||
final ConstructListModel constructList = ConstructListModel(
|
||||
uses: constructUses,
|
||||
type: null,
|
||||
);
|
||||
|
||||
final List<TokenWithXP> tokenCounts = [];
|
||||
|
||||
// TODO - add morph constructs to this list as well
|
||||
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;
|
||||
}
|
||||
|
||||
tokenCounts.add(tokens[i].emptyTokenWithXP);
|
||||
|
||||
for (final construct in tokenCounts.last.constructs) {
|
||||
final constructUseModel = constructList.getConstructUses(
|
||||
construct.id.lemma,
|
||||
construct.id.type,
|
||||
);
|
||||
if (constructUseModel != null) {
|
||||
construct.xp = constructUseModel.points;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokenCounts.sort((a, b) => a.xp.compareTo(b.xp));
|
||||
|
||||
return tokenCounts;
|
||||
}
|
||||
|
||||
void setCompletionRecord(PracticeActivityRecordModel? recordModel) {
|
||||
currentCompletionRecord = recordModel;
|
||||
}
|
||||
|
||||
/// future that simply waits for the appropriate time to savor the joy
|
||||
Future<void> savorTheJoy() async {
|
||||
joyTimer?.cancel();
|
||||
if (savoringTheJoy) return;
|
||||
savoringTheJoy = true;
|
||||
joyTimer = Timer(appropriateTimeForJoy, () {
|
||||
savoringTheJoy = false;
|
||||
joyTimer?.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
/// If the currentActivity is the last activity, the method sets the appropriate flag to true.
|
||||
/// If the currentActivity is not the last activity, the method fetches a new activity.
|
||||
void onActivityFinish() async {
|
||||
try {
|
||||
if (currentCompletionRecord == null || currentActivity == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return;
|
||||
}
|
||||
|
||||
// start joy timer
|
||||
savorTheJoy();
|
||||
|
||||
// if this is the last activity, set the flag to true
|
||||
// so we can give them some kudos
|
||||
if (widget.overlayController.activitiesLeftToComplete == 1) {
|
||||
widget.overlayController.finishedActivitiesThisSession = true;
|
||||
}
|
||||
|
||||
final Event? event = await MatrixState
|
||||
.pangeaController.activityRecordController
|
||||
.send(currentCompletionRecord!, currentActivity!);
|
||||
|
||||
MatrixState.pangeaController.myAnalytics.setState(
|
||||
AnalyticsStream(
|
||||
eventId: widget.pangeaMessageEvent.eventId,
|
||||
eventType: PangeaEventTypes.activityRecord,
|
||||
roomId: event!.room.id,
|
||||
practiceActivity: currentActivity!,
|
||||
recordModel: currentCompletionRecord!,
|
||||
),
|
||||
);
|
||||
|
||||
if (!widget.overlayController.finishedActivitiesThisSession) {
|
||||
currentActivity = await _fetchNewActivity();
|
||||
|
||||
currentActivity == null
|
||||
? widget.overlayController.exitPracticeFlow()
|
||||
: widget.overlayController
|
||||
.onNewActivity(currentActivity!.practiceActivity);
|
||||
} else {
|
||||
updateFetchingActivity(false);
|
||||
widget.overlayController.setState(() {});
|
||||
}
|
||||
} catch (e, s) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
e: error,
|
||||
s: StackTrace.current,
|
||||
e: e,
|
||||
s: s,
|
||||
m: 'Failed to send record for 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(
|
||||
widget.overlayController.exitPracticeFlow();
|
||||
}
|
||||
}
|
||||
|
||||
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 the user has finished all the activities to unlock the toolbar in this session
|
||||
if (widget.overlayController.finishedActivitiesThisSession) {
|
||||
return "Boom! Tools unlocked!";
|
||||
|
||||
// if we have no activities to show
|
||||
} else 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,
|
||||
if (userMessage != null) {
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 80,
|
||||
),
|
||||
child: Text(
|
||||
userMessage!,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
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,
|
||||
// );
|
||||
}
|
||||
return Column(
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
PracticeActivity(
|
||||
practiceEvent: currentActivity!,
|
||||
controller: this,
|
||||
// Main content
|
||||
const Positioned(
|
||||
child: PointsGainedAnimation(),
|
||||
),
|
||||
navigationButtons,
|
||||
Column(
|
||||
children: [
|
||||
activityWidget,
|
||||
// navigationButtons,
|
||||
],
|
||||
),
|
||||
// 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(),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@ Future<void> pLanguageDialog(
|
|||
future: () async {
|
||||
try {
|
||||
pangeaController.myAnalytics
|
||||
.updateAnalytics()
|
||||
.sendLocalAnalyticsToAnalyticsRoom()
|
||||
.then((_) {
|
||||
pangeaController.userController.updateProfile(
|
||||
(profile) {
|
||||
|
|
|
|||
36
pubspec.lock
36
pubspec.lock
|
|
@ -1305,18 +1305,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.5"
|
||||
version: "10.0.4"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.3"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1417,10 +1417,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
version: "0.8.0"
|
||||
material_symbols_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -1442,10 +1442,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.12.0"
|
||||
mgrs_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1682,10 +1682,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
version: "3.1.4"
|
||||
platform_detect:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -2303,26 +2303,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e"
|
||||
sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.25.7"
|
||||
version: "1.25.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
version: "0.7.0"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696"
|
||||
sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.4"
|
||||
version: "0.6.0"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -2615,10 +2615,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.5"
|
||||
version: "14.2.1"
|
||||
wakelock_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue