diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index beb8fc820..2ebb4c1ee 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4705,5 +4705,6 @@ "lemmasNeverUsedCorrectly": "Number of lemmas used correctly 0 times", "available": "Available", "unavailable": "Unavailable", - "accessingMemberAnalytics": "Accessing member analytics..." -} + "accessingMemberAnalytics": "Accessing member analytics...", + "editLemmaMeaning": "Pangea Bot makes mistakes too! What should be the definition of this lemma?" +} \ No newline at end of file diff --git a/lib/pangea/constants/model_keys.dart b/lib/pangea/constants/model_keys.dart index f58f70755..5329a3187 100644 --- a/lib/pangea/constants/model_keys.dart +++ b/lib/pangea/constants/model_keys.dart @@ -81,6 +81,15 @@ class ModelKey { static const String tokensWritten = "tokens_written"; static const String choreoRecord = "choreo_record"; + /// This is strictly for use in message content jsons + /// in order to flag that the message edit was done in order + /// to edit some message data such as tokens, morph tags, etc. + /// This will help us know to omit the message from notifications, + /// bot responses, etc. It will also help use find the message if + /// we want to gather user edits for LLM fine-tuning. + static const String messageTags = "p.tag"; + static const String messageTagMorphEdit = "morph_edit"; + static const String baseDefinition = "base_definition"; static const String targetDefinition = "target_definition"; static const String basePartOfSpeech = "base_part_of_speech"; diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index 6aa6dc793..c69d3863d 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -4,18 +4,8 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:html_unescape/html_unescape.dart'; -import 'package:matrix/matrix.dart' as matrix; -import 'package:matrix/matrix.dart'; -import 'package:matrix/src/utils/markdown.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - import 'package:fluffychat/pangea/constants/bot_mode.dart'; import 'package:fluffychat/pangea/constants/class_code_constants.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; @@ -31,6 +21,15 @@ import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/future_loading_dialog.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:html_unescape/html_unescape.dart'; +import 'package:matrix/matrix.dart' as matrix; +import 'package:matrix/matrix.dart'; +import 'package:matrix/src/utils/markdown.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + import '../../../config/app_config.dart'; import '../../constants/pangea_event_types.dart'; import '../../models/choreo_record.dart'; @@ -178,6 +177,7 @@ extension PangeaRoom on Room { PangeaMessageTokens? tokensSent, PangeaMessageTokens? tokensWritten, ChoreoRecord? choreo, + String? messageTag, }) => _pangeaSendTextEvent( message, @@ -194,6 +194,7 @@ extension PangeaRoom on Room { tokensSent: tokensSent, tokensWritten: tokensWritten, choreo: choreo, + messageTag: messageTag, ); Future updateStateEvent(Event stateEvent) => diff --git a/lib/pangea/extensions/pangea_room_extension/room_events_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_events_extension.dart index 65246bbbe..8ac606025 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_events_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_events_extension.dart @@ -236,6 +236,7 @@ extension EventsRoomExtension on Room { PangeaMessageTokens? tokensSent, PangeaMessageTokens? tokensWritten, ChoreoRecord? choreo, + String? messageTag, }) { // if (parseCommands) { // return client.parseAndRunCommand(this, message, @@ -248,12 +249,26 @@ extension EventsRoomExtension on Room { final event = { 'msgtype': msgtype, 'body': message, - ModelKey.choreoRecord: choreo?.toJson(), - ModelKey.originalSent: originalSent?.toJson(), - ModelKey.originalWritten: originalWritten?.toJson(), - ModelKey.tokensSent: tokensSent?.toJson(), - ModelKey.tokensWritten: tokensWritten?.toJson(), }; + if (choreo != null) { + event[ModelKey.choreoRecord] = choreo.toJson(); + } + if (originalSent != null) { + event[ModelKey.originalSent] = originalSent.toJson(); + } + if (originalWritten != null) { + event[ModelKey.originalWritten] = originalWritten.toJson(); + } + if (tokensSent != null) { + event[ModelKey.tokensSent] = tokensSent.toJson(); + } + if (tokensWritten != null) { + event[ModelKey.tokensWritten] = tokensWritten.toJson(); + } + if (messageTag != null) { + event[ModelKey.messageTags] = messageTag; + } + if (parseMarkdown) { final html = markdown( event['body'], diff --git a/lib/pangea/repo/lemma_info/lemma_info_repo.dart b/lib/pangea/repo/lemma_info/lemma_info_repo.dart index 6eedf5c71..a584201d6 100644 --- a/lib/pangea/repo/lemma_info/lemma_info_repo.dart +++ b/lib/pangea/repo/lemma_info/lemma_info_repo.dart @@ -1,16 +1,15 @@ import 'dart:convert'; import 'dart:developer'; -import 'package:flutter/foundation.dart'; - -import 'package:http/http.dart'; - import 'package:fluffychat/pangea/models/content_feedback.dart'; import 'package:fluffychat/pangea/network/urls.dart'; import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_request.dart'; import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_response.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; + import '../../config/environment.dart'; import '../../network/requests.dart'; @@ -19,7 +18,14 @@ class LemmaInfoRepo { static final Map _cache = {}; static final Map _cacheTimestamps = {}; - static const Duration _cacheDuration = Duration(days: 2); + static const Duration _cacheDuration = Duration(days: 30); + + static void set(LemmaInfoRequest request, LemmaInfoResponse response) { + _cache[request] = response; + + // set it to sometime in the future so we keep it in the cache for a while + _cacheTimestamps[request] = DateTime.now().add(const Duration(days: 365)); + } static Future get( LemmaInfoRequest request, [ diff --git a/lib/pangea/widgets/word_zoom/lemma_meaning_widget.dart b/lib/pangea/widgets/word_zoom/lemma_meaning_widget.dart index f86007de3..2e3a0eff0 100644 --- a/lib/pangea/widgets/word_zoom/lemma_meaning_widget.dart +++ b/lib/pangea/widgets/word_zoom/lemma_meaning_widget.dart @@ -1,14 +1,15 @@ import 'dart:developer'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - +import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/choreographer/widgets/text_loading_shimmer.dart'; import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_repo.dart'; import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_request.dart'; -import 'package:fluffychat/utils/feedback_dialog.dart'; +import 'package:fluffychat/pangea/repo/lemma_info/lemma_info_response.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'; class LemmaMeaningWidget extends StatefulWidget { final String lemma; @@ -27,56 +28,57 @@ class LemmaMeaningWidget extends StatefulWidget { } class LemmaMeaningWidgetState extends State { - late Future _definitionFuture; + bool _editMode = false; + late TextEditingController _controller; @override void initState() { super.initState(); - _definitionFuture = _fetchDefinition(); + _controller = TextEditingController(); } - Future _fetchDefinition([String? feedback]) async { - final LemmaInfoRequest lemmaDefReq = LemmaInfoRequest( - lemma: widget.lemma, - partOfSpeech: widget.pos, - - /// This assumes that the user's L2 is the language of the lemma - lemmaLang: widget.langCode, - userL1: - MatrixState.pangeaController.languageController.userL1?.langCode ?? - LanguageKeys.defaultLanguage, - ); - - final res = await LemmaInfoRepo.get(lemmaDefReq, feedback); - return res.meaning; + @override + void dispose() { + _controller.dispose(); + super.dispose(); } - void _showFeedbackDialog(String offendingContentString) async { - // setState(() { - // _definitionFuture = _fetchDefinition(offendingContentString); - // }); - await showFeedbackDialog( - context, - Text(offendingContentString), - (feedback) async { - setState(() { - _definitionFuture = _fetchDefinition(feedback); - }); - return; - }, + LemmaInfoRequest get _request => LemmaInfoRequest( + lemma: widget.lemma, + partOfSpeech: widget.pos, + + /// This assumes that the user's L2 is the language of the lemma + lemmaLang: widget.langCode, + userL1: + MatrixState.pangeaController.languageController.userL1?.langCode ?? + LanguageKeys.defaultLanguage, + ); + + Future _lemmaMeaning() => LemmaInfoRepo.get(_request); + + void _toggleEditMode(bool value) => setState(() => _editMode = value); + + Future editLemmaMeaning(String userEdit) async { + final originalMeaning = await _lemmaMeaning(); + + LemmaInfoRepo.set( + _request, + LemmaInfoResponse(emoji: originalMeaning.emoji, meaning: userEdit), ); + + _toggleEditMode(false); } @override Widget build(BuildContext context) { - return FutureBuilder( - future: _definitionFuture, + return FutureBuilder( + future: _lemmaMeaning(), builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const TextLoadingShimmer(); } - if (snapshot.hasError) { + if (snapshot.hasError || snapshot.data == null) { debugger(when: kDebugMode); return Text( snapshot.error.toString(), @@ -84,11 +86,60 @@ class LemmaMeaningWidgetState extends State { ); } + if (_editMode) { + _controller.text = snapshot.data!.meaning; + return Container( + constraints: const BoxConstraints( + maxWidth: AppConfig.toolbarMinWidth, + maxHeight: 160, + ), + child: Column( + children: [ + Text( + L10n.of(context).editLemmaMeaning, + textAlign: TextAlign.center, + style: const TextStyle(fontStyle: FontStyle.italic), + ), + const SizedBox(height: 10), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: TextField( + controller: _controller, + onSubmitted: editLemmaMeaning, + decoration: InputDecoration( + hintText: snapshot.data!.meaning, + ), + ), + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () => _toggleEditMode(false), + child: Text(L10n.of(context).cancel), + ), + const SizedBox(width: 10), + ElevatedButton( + onPressed: () => + _controller.text != snapshot.data!.meaning && + _controller.text.isNotEmpty + ? editLemmaMeaning(_controller.text) + : null, + child: Text(L10n.of(context).saveChanges), + ), + ], + ), + ], + ), + ); + } + return GestureDetector( - onLongPress: () => _showFeedbackDialog(snapshot.data as String), - onDoubleTap: () => _showFeedbackDialog(snapshot.data as String), + onLongPress: () => _toggleEditMode(true), + onDoubleTap: () => _toggleEditMode(true), child: Text( - snapshot.data as String, + snapshot.data!.meaning, textAlign: TextAlign.center, ), ); diff --git a/lib/pangea/widgets/word_zoom/morphs/morphological_center_widget.dart b/lib/pangea/widgets/word_zoom/morphs/morphological_center_widget.dart index 8b7b415b9..86ed562ce 100644 --- a/lib/pangea/widgets/word_zoom/morphs/morphological_center_widget.dart +++ b/lib/pangea/widgets/word_zoom/morphs/morphological_center_widget.dart @@ -1,29 +1,30 @@ // stateful widget that displays morphological label and a shimmer effect while the text is loading // takes a token and morphological feature as input -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - +import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/morph_categories_and_labels.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; -import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/grammar/get_grammar_copy.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; class MorphologicalCenterWidget extends StatefulWidget { final PangeaToken token; final String morphFeature; final PangeaMessageEvent pangeaMessageEvent; + final MessageOverlayController overlayController; const MorphologicalCenterWidget({ required this.token, required this.morphFeature, required this.pangeaMessageEvent, + required this.overlayController, super.key, }); @@ -88,6 +89,8 @@ class MorphologicalCenterWidgetState extends State { // send a new message as an edit to original message to the server // including the new tokens + // marking the message as a morphological edit will allow use to filter + // from some processing and potentially find the data for LLM fine-tuning await pm.room.pangeaSendTextEvent( pm.messageDisplayText, editEventId: pm.eventId, @@ -98,7 +101,12 @@ class MorphologicalCenterWidgetState extends State { ? PangeaMessageTokens(tokens: pm.originalWritten!.tokens!) : null, choreo: pm.originalSent?.choreo, + messageTag: ModelKey.messageTagMorphEdit, ); + + setState(() { + editMode = false; + }); } catch (e) { SnackBar( content: Text(L10n.of(context).oopsSomethingWentWrong), @@ -117,8 +125,10 @@ class MorphologicalCenterWidgetState extends State { List get allMorphTagsForEdit { final List tags = getLabelsForMorphCategory(widget.morphFeature) - .where((tag) => !["punct", "space", "sym", "x", "other"] - .contains(tag.toLowerCase())) + .where( + (tag) => !["punct", "space", "sym", "x", "other"] + .contains(tag.toLowerCase()), + ) .toList(); // as long as the feature is not POS, add a nan tag @@ -153,6 +163,7 @@ class MorphologicalCenterWidgetState extends State { Text( L10n.of(context).editMorphologicalLabel, textAlign: TextAlign.center, + style: const TextStyle(fontStyle: FontStyle.italic), ), const SizedBox(height: 10), Container( @@ -206,7 +217,6 @@ class MorphologicalCenterWidgetState extends State { context: context, ) ?? tag, - style: BotStyle.text(context), textAlign: TextAlign.center, ), ), diff --git a/lib/pangea/widgets/word_zoom/word_zoom_center_widget.dart b/lib/pangea/widgets/word_zoom/word_zoom_center_widget.dart index 720ca05b8..aff842ed9 100644 --- a/lib/pangea/widgets/word_zoom/word_zoom_center_widget.dart +++ b/lib/pangea/widgets/word_zoom/word_zoom_center_widget.dart @@ -1,15 +1,15 @@ import 'dart:developer'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - import 'package:fluffychat/pangea/controllers/message_analytics_controller.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:fluffychat/pangea/widgets/word_zoom/lemma_meaning_widget.dart'; import 'package:fluffychat/pangea/widgets/word_zoom/morphs/morphological_center_widget.dart'; import 'package:fluffychat/pangea/widgets/word_zoom/word_zoom_widget.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; class WordZoomCenterWidget extends StatelessWidget { final WordZoomSelection? selectionType; @@ -27,6 +27,9 @@ class WordZoomCenterWidget extends StatelessWidget { super.key, }); + MessageOverlayController get overlayController => + wordDetailsController.widget.overlayController; + PangeaToken get token => wordDetailsController.widget.token; Widget content(BuildContext context, WordZoomSelection selectionType) { @@ -40,6 +43,7 @@ class WordZoomCenterWidget extends StatelessWidget { token: token, morphFeature: selectedMorphFeature!, pangeaMessageEvent: wordDetailsController.widget.messageEvent, + overlayController: overlayController, ); case WordZoomSelection.lemma: return Text(token.lemma.text, textAlign: TextAlign.center);