diff --git a/lib/pangea/choreographer/models/igc_text_data_model.dart b/lib/pangea/choreographer/models/igc_text_data_model.dart index aa75fc33b..ba1574554 100644 --- a/lib/pangea/choreographer/models/igc_text_data_model.dart +++ b/lib/pangea/choreographer/models/igc_text_data_model.dart @@ -10,7 +10,7 @@ import 'package:fluffychat/pangea/choreographer/models/language_detection_model. import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/choreographer/models/span_card_model.dart'; import 'package:fluffychat/pangea/choreographer/models/span_data.dart'; -import 'package:fluffychat/pangea/choreographer/repo/language_detection_request.dart'; +import 'package:fluffychat/pangea/choreographer/repo/language_detection_repo.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_representation_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; @@ -21,7 +21,7 @@ import '../../common/constants/model_keys.dart'; // import 'package:language_tool/language_tool.dart'; class IGCTextData { - LanguageDetectionRequest detections; + LanguageDetectionResponse detections; String originalInput; String? fullTextCorrection; List tokens; @@ -47,14 +47,15 @@ class IGCTextData { factory IGCTextData.fromJson(Map json) { // changing this to allow for use of the LanguageDetectionResponse methods // TODO - change API after we're sure all clients are updated. not urgent. - final LanguageDetectionRequest detections = json[_detectionsKey] is Iterable - ? LanguageDetectionRequest.fromJson({ - "detections": json[_detectionsKey], - "full_text": json["original_input"], - }) - : LanguageDetectionRequest.fromJson( - json[_detectionsKey] as Map, - ); + final LanguageDetectionResponse detections = + json[_detectionsKey] is Iterable + ? LanguageDetectionResponse.fromJson({ + "detections": json[_detectionsKey], + "full_text": json["original_input"], + }) + : LanguageDetectionResponse.fromJson( + json[_detectionsKey] as Map, + ); return IGCTextData( tokens: (json[_tokensKey] as Iterable) @@ -102,7 +103,7 @@ class IGCTextData { } return IGCTextData( - detections: LanguageDetectionRequest( + detections: LanguageDetectionResponse( detections: [ LanguageDetection(langCode: content.langCode, confidence: 1), ], @@ -136,7 +137,7 @@ class IGCTextData { }; /// if we haven't run IGC or IT or there are no matches, we use the highest validated detection - /// from [LanguageDetectionRequest.highestValidatedDetection] + /// from [LanguageDetectionResponse.highestValidatedDetection] /// if we have run igc/it and there are no matches, we can relax the threshold /// and use the highest confidence detection String get detectedLanguage { diff --git a/lib/pangea/choreographer/repo/igc_repo.dart b/lib/pangea/choreographer/repo/igc_repo.dart index 4cad3c98e..5faafc501 100644 --- a/lib/pangea/choreographer/repo/igc_repo.dart +++ b/lib/pangea/choreographer/repo/igc_repo.dart @@ -4,7 +4,7 @@ import 'package:http/http.dart'; import 'package:fluffychat/pangea/choreographer/models/language_detection_model.dart'; import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; -import 'package:fluffychat/pangea/choreographer/repo/language_detection_request.dart'; +import 'package:fluffychat/pangea/choreographer/repo/language_detection_repo.dart'; import 'package:fluffychat/pangea/choreographer/repo/span_data_repo.dart'; import 'package:fluffychat/pangea/common/config/environment.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; @@ -41,7 +41,7 @@ class IgcRepo { await Future.delayed(const Duration(seconds: 2)); final IGCTextData igcTextData = IGCTextData( - detections: LanguageDetectionRequest( + detections: LanguageDetectionResponse( detections: [LanguageDetection(langCode: "en", confidence: 0.99)], fullText: "This be a sample text", ), diff --git a/lib/pangea/choreographer/repo/language_detection_request.dart b/lib/pangea/choreographer/repo/language_detection_repo.dart similarity index 53% rename from lib/pangea/choreographer/repo/language_detection_request.dart rename to lib/pangea/choreographer/repo/language_detection_repo.dart index ac1b057c1..97c114417 100644 --- a/lib/pangea/choreographer/repo/language_detection_request.dart +++ b/lib/pangea/choreographer/repo/language_detection_repo.dart @@ -1,17 +1,68 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; + import 'package:fluffychat/pangea/choreographer/models/language_detection_model.dart'; +import 'package:fluffychat/pangea/common/config/environment.dart'; +import 'package:fluffychat/pangea/common/network/requests.dart'; +import 'package:fluffychat/pangea/common/network/urls.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; +class LanguageDetectionRepo { + static Future get( + String? accessToken, { + required LanguageDetectionRequest request, + }) async { + final Requests req = Requests( + accessToken: accessToken, + choreoApiKey: Environment.choreoApiKey, + ); + final Response res = await req.post( + url: PApiUrls.languageDetection, + body: request.toJson(), + ); + + final Map json = + jsonDecode(utf8.decode(res.bodyBytes).toString()); + + final LanguageDetectionResponse response = + LanguageDetectionResponse.fromJson(json); + + return response; + } +} + class LanguageDetectionRequest { + final String text; + final String? senderl1; + final String? senderl2; + + LanguageDetectionRequest({ + required this.text, + this.senderl1, + this.senderl2, + }); + + Map toJson() { + return { + 'full_text': text, + 'sender_l1': senderl1, + 'sender_l2': senderl2, + }; + } +} + +class LanguageDetectionResponse { List detections; String fullText; - LanguageDetectionRequest({ + LanguageDetectionResponse({ required this.detections, required this.fullText, }); - factory LanguageDetectionRequest.fromJson(Map json) { - return LanguageDetectionRequest( + factory LanguageDetectionResponse.fromJson(Map json) { + return LanguageDetectionResponse( detections: List.from( (json['detections'] as Iterable).map( (e) => LanguageDetection.fromJson(e), diff --git a/lib/pangea/events/controllers/message_data_controller.dart b/lib/pangea/events/controllers/message_data_controller.dart index d8a34e32b..4d91e287e 100644 --- a/lib/pangea/events/controllers/message_data_controller.dart +++ b/lib/pangea/events/controllers/message_data_controller.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/pangea/common/controllers/base_controller.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/common/network/requests.dart'; import 'package:fluffychat/pangea/common/network/urls.dart'; +import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/representation_content_model.dart'; import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'; @@ -184,4 +185,70 @@ class MessageDataController extends BaseController { return rep; } + + Future getPangeaRepresentationEvent({ + required FullTextTranslationRequestModel req, + required PangeaMessageEvent messageEvent, + bool originalSent = false, + }) async { + final FullTextTranslationResponseModel res = + await FullTextTranslationRepo.translate( + accessToken: _pangeaController.userController.accessToken, + request: req, + ); + + if (originalSent && messageEvent.originalSent != null) { + originalSent = false; + } + + final rep = PangeaRepresentation( + langCode: req.tgtLang, + text: res.bestTranslation, + originalSent: originalSent, + originalWritten: false, + ); + + try { + final repEvent = await messageEvent.room.sendPangeaEvent( + content: rep.toJson(), + parentEventId: messageEvent.eventId, + type: PangeaEventTypes.representation, + ); + return repEvent?.eventId; + } catch (e, s) { + ErrorHandler.logError( + m: "error in _getPangeaRepresentation.sendPangeaEvent", + e: e, + s: s, + data: req.toJson(), + ); + return null; + } + } + + Future sendTokensEvent({ + required String repEventId, + required TokensRequestModel req, + required Room room, + }) async { + final TokensResponseModel res = await _fetchTokens( + _pangeaController.userController.accessToken, + req, + ); + + try { + await room.sendPangeaEvent( + content: PangeaMessageTokens(tokens: res.tokens).toJson(), + parentEventId: repEventId, + type: PangeaEventTypes.tokens, + ); + } catch (e, s) { + ErrorHandler.logError( + m: "error in _getTokens.sendPangeaEvent", + e: e, + s: s, + data: req.toJson(), + ); + } + } } diff --git a/lib/pangea/events/event_wrappers/pangea_message_event.dart b/lib/pangea/events/event_wrappers/pangea_message_event.dart index b0ddd2315..22a8470a8 100644 --- a/lib/pangea/events/event_wrappers/pangea_message_event.dart +++ b/lib/pangea/events/event_wrappers/pangea_message_event.dart @@ -10,6 +10,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart'; import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/choreographer/repo/full_text_translation_repo.dart'; +import 'package:fluffychat/pangea/choreographer/repo/language_detection_repo.dart'; import 'package:fluffychat/pangea/common/constants/model_keys.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_representation_event.dart'; import 'package:fluffychat/pangea/events/extensions/pangea_event_extension.dart'; @@ -508,6 +509,39 @@ class PangeaMessageEvent { ); } + Future representationByDetectedLanguage() async { + final resp = await LanguageDetectionRepo.get( + MatrixState.pangeaController.userController.accessToken, + request: LanguageDetectionRequest( + text: _latestEdit.body, + senderl1: l1Code, + senderl2: l2Code, + ), + ); + + final langCode = resp.detections.firstOrNull?.langCode; + if (langCode == null) return null; + if (langCode == originalSent?.langCode) { + return originalSent?.event?.eventId; + } + + // clear representations cache so the new representation event can be added when next requested + _representations = null; + + return MatrixState.pangeaController.messageData + .getPangeaRepresentationEvent( + req: FullTextTranslationRequestModel( + text: originalSent?.content.text ?? _latestEdit.body, + srcLang: originalSent?.langCode, + tgtLang: langCode, + userL2: l2Code ?? LanguageKeys.unknownLanguage, + userL1: l1Code ?? LanguageKeys.unknownLanguage, + ), + messageEvent: this, + originalSent: true, + ); + } + RepresentationEvent? get originalSent => representations .firstWhereOrNull((element) => element.content.originalSent); diff --git a/lib/pangea/events/event_wrappers/pangea_representation_event.dart b/lib/pangea/events/event_wrappers/pangea_representation_event.dart index 91c4d3878..4ed902f5a 100644 --- a/lib/pangea/events/event_wrappers/pangea_representation_event.dart +++ b/lib/pangea/events/event_wrappers/pangea_representation_event.dart @@ -160,6 +160,33 @@ class RepresentationEvent { return res; } + Future sendTokensEvent( + String repEventID, + Room room, + String userl1, + String userl2, + ) async { + if (tokens != null) return; + if (_event == null) { + ErrorHandler.logError( + e: "Called getTokensEvent with no _event", + data: {}, + ); + return; + } + + await MatrixState.pangeaController.messageData.sendTokensEvent( + repEventId: repEventID, + room: room, + req: TokensRequestModel( + fullText: text, + langCode: langCode, + senderL1: userl1, + senderL2: userl2, + ), + ); + } + ChoreoRecord? get choreo { if (_choreo != null) return _choreo; diff --git a/lib/pangea/toolbar/widgets/message_selection_overlay.dart b/lib/pangea/toolbar/widgets/message_selection_overlay.dart index 79e85ed2f..93b3be657 100644 --- a/lib/pangea/toolbar/widgets/message_selection_overlay.dart +++ b/lib/pangea/toolbar/widgets/message_selection_overlay.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/analytics_misc/message_analytics_controller.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/events/event_wrappers/pangea_representation_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; import 'package:fluffychat/pangea/toolbar/controllers/text_to_speech_controller.dart'; @@ -52,8 +53,6 @@ class MessageOverlayController extends State MessageMode toolbarMode = MessageMode.noneSelected; PangeaTokenText? _selectedSpan; List? _highlightedTokens; - - List? tokens; bool initialized = false; PangeaMessageEvent? get pangeaMessageEvent => PangeaMessageEvent( @@ -143,20 +142,43 @@ class MessageOverlayController extends State } MessageAnalyticsEntry? get messageAnalyticsEntry => - pangeaMessageEvent != null && tokens != null + pangeaMessageEvent?.messageDisplayRepresentation?.tokens != null ? MatrixState.pangeaController.getAnalytics.perMessage.get( - tokens!, + pangeaMessageEvent!.messageDisplayRepresentation!.tokens!, pangeaMessageEvent!, ) : null; + Future _fetchNewRepEvent() async { + final RepresentationEvent? repEvent = + pangeaMessageEvent?.messageDisplayRepresentation; + + if (repEvent != null) return repEvent; + final eventID = + await pangeaMessageEvent?.representationByDetectedLanguage(); + + if (eventID == null) return null; + final event = await widget._event.room.getEventById(eventID); + if (event == null) return null; + return RepresentationEvent( + timeline: pangeaMessageEvent!.timeline, + parentMessageEvent: pangeaMessageEvent!.event, + event: event, + ); + } + Future initializeTokensAndMode() async { try { - final repEvent = pangeaMessageEvent?.messageDisplayRepresentation; - if (repEvent != null) { - tokens = await repEvent.tokensGlobal( - pangeaMessageEvent!.senderId, - pangeaMessageEvent!.originServerTs, + RepresentationEvent? repEvent = + pangeaMessageEvent?.messageDisplayRepresentation; + repEvent ??= await _fetchNewRepEvent(); + + if (repEvent?.event != null) { + await repEvent!.sendTokensEvent( + repEvent.event!.eventId, + widget._event.room, + MatrixState.pangeaController.languageController.userL1!.langCode, + MatrixState.pangeaController.languageController.userL2!.langCode, ); } } catch (e, s) { @@ -331,7 +353,9 @@ class MessageOverlayController extends State ); } - PangeaToken? get selectedToken => tokens?.firstWhereOrNull(isTokenSelected); + PangeaToken? get selectedToken => + pangeaMessageEvent?.messageDisplayRepresentation?.tokens + ?.firstWhereOrNull(isTokenSelected); /// Whether the overlay is currently displaying a selection bool get isSelection => _selectedSpan != null || _highlightedTokens != null; diff --git a/lib/pangea/toolbar/widgets/message_token_text.dart b/lib/pangea/toolbar/widgets/message_token_text.dart index 7098ad42c..c6587d4f2 100644 --- a/lib/pangea/toolbar/widgets/message_token_text.dart +++ b/lib/pangea/toolbar/widgets/message_token_text.dart @@ -16,9 +16,6 @@ import 'package:fluffychat/widgets/matrix.dart'; /// Need to test. class MessageTokenText extends StatelessWidget { final PangeaMessageEvent _pangeaMessageEvent; - - final List? _tokens; - final TextStyle _style; final bool Function(PangeaToken)? _isSelected; @@ -34,9 +31,11 @@ class MessageTokenText extends StatelessWidget { }) : _onClick = onClick, _isSelected = isSelected, _style = style, - _tokens = tokens, _pangeaMessageEvent = pangeaMessageEvent; + List? get _tokens => + _pangeaMessageEvent.messageDisplayRepresentation?.tokens; + MessageAnalyticsEntry? get messageAnalyticsEntry => _tokens != null ? MatrixState.pangeaController.getAnalytics.perMessage.get( _tokens!, @@ -44,6 +43,12 @@ class MessageTokenText extends StatelessWidget { ) : null; + void callOnClick(TokenPosition tokenPosition) { + _onClick != null && tokenPosition.token != null + ? _onClick!(tokenPosition.token!) + : null; + } + @override Widget build(BuildContext context) { if (_tokens == null) { @@ -53,12 +58,6 @@ class MessageTokenText extends StatelessWidget { ); } - void callOnClick(TokenPosition tokenPosition) { - _onClick != null && tokenPosition.token != null - ? _onClick!(tokenPosition.token!) - : null; - } - return MessageTextWidget( pangeaMessageEvent: _pangeaMessageEvent, style: _style, diff --git a/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart index bab6a0156..68ca7a616 100644 --- a/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/toolbar/widgets/practice_activity/practice_activity_card.dart @@ -200,7 +200,8 @@ class PracticeActivityCardState extends State { userL1: MatrixState.pangeaController.languageController.userL1!.langCode, userL2: MatrixState.pangeaController.languageController.userL2!.langCode, messageText: widget.pangeaMessageEvent.messageDisplayText, - messageTokens: widget.overlayController.tokens!, + messageTokens: + widget.pangeaMessageEvent.messageDisplayRepresentation?.tokens ?? [], activityQualityFeedback: activityFeedback, targetTokens: tokens, targetType: type,