Morph edits (#1398)
* feat: allow user to edit morph tags * fix: not local choreeoooooo * fix: reverting pangea representation * dev: remove first implementation --------- Co-authored-by: ggurdin <ggurdin@gmail.com>
This commit is contained in:
parent
8cbe1ea1f7
commit
075b3da80e
8 changed files with 338 additions and 102 deletions
|
|
@ -4230,6 +4230,7 @@
|
|||
"reportContentIssueTitle": "Report content issue",
|
||||
"feedback": "Optional feedback",
|
||||
"reportContentIssueDescription": "Uh oh! AI can faciliate personalized learning experiences but... also hallucinates. Please provide any feedback you have and we'll try again.",
|
||||
"changeContent": "Uh oh! AI can faciliate personalized learning experiences but... also hallucinates. What should it be?",
|
||||
"clickTheWordAgainToDeselect": "Click the selected word to deselect it.",
|
||||
"l2SupportNa": "Not Available",
|
||||
"l2SupportAlpha": "Alpha",
|
||||
|
|
@ -4699,6 +4700,7 @@
|
|||
"downloading": "Downloading...",
|
||||
"failedFetchUserAnalytics": "Failed to download user analytics",
|
||||
"downloadComplete": "Download complete!",
|
||||
"editMorphologicalLabel": "Pangea Bot makes mistakes too! What should this label be?",
|
||||
"dataAvailable": "Data availability",
|
||||
"lemmasNeverUsedCorrectly": "Number of lemmas used correctly 0 times",
|
||||
"available": "Available",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ abstract class AppConfig {
|
|||
static const double messageFontSize = 16.0;
|
||||
static const bool allowOtherHomeservers = true;
|
||||
static const bool enableRegistration = true;
|
||||
static const double toolbarMaxHeight = 300.0;
|
||||
static const double toolbarMaxHeight = 440.0;
|
||||
static const double toolbarMinHeight = 175.0;
|
||||
static const double toolbarMinWidth = 350.0;
|
||||
static const double toolbarButtonsHeight = 50.0;
|
||||
|
|
|
|||
|
|
@ -225,3 +225,12 @@ IconData getIconForMorphFeature(String feature) {
|
|||
return Icons.help_outline;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> getLabelsForMorphCategory(String category) {
|
||||
for (final feat in morphCategoriesAndLabels.keys) {
|
||||
if (feat.toLowerCase() == category.toLowerCase()) {
|
||||
return morphCategoriesAndLabels[feat]!;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,247 @@
|
|||
// 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/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/widgets/future_loading_dialog.dart';
|
||||
|
||||
class MorphologicalCenterWidget extends StatefulWidget {
|
||||
final PangeaToken token;
|
||||
final String morphFeature;
|
||||
final PangeaMessageEvent pangeaMessageEvent;
|
||||
|
||||
const MorphologicalCenterWidget({
|
||||
required this.token,
|
||||
required this.morphFeature,
|
||||
required this.pangeaMessageEvent,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
MorphologicalCenterWidgetState createState() =>
|
||||
MorphologicalCenterWidgetState();
|
||||
}
|
||||
|
||||
class MorphologicalCenterWidgetState extends State<MorphologicalCenterWidget> {
|
||||
bool editMode = false;
|
||||
|
||||
/// the morphological tag that the user has selected in edit mode
|
||||
String selectedMorphTag = "";
|
||||
|
||||
void resetMorphTag() => setState(
|
||||
() => selectedMorphTag = widget.token.morph[widget.morphFeature]!,
|
||||
);
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MorphologicalCenterWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.token != oldWidget.token ||
|
||||
widget.morphFeature != oldWidget.morphFeature) {
|
||||
resetMorphTag();
|
||||
setState(() => editMode = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
resetMorphTag();
|
||||
}
|
||||
|
||||
void enterEditMode() {
|
||||
setState(() {
|
||||
editMode = true;
|
||||
});
|
||||
}
|
||||
|
||||
PangeaMessageEvent get pm => widget.pangeaMessageEvent;
|
||||
|
||||
Future<void> confirmChanges() async {
|
||||
try {
|
||||
// NOTE: it is not clear how this would work if the user was not editing the originalSent tokens
|
||||
// this case would only happen in immersion mode which is disabled until further notice
|
||||
// this flow assumes that the user is editing the originalSent tokens
|
||||
// if not, we'll get an error and we'll cross that bridge
|
||||
|
||||
// make a copy of the original tokens
|
||||
final existingTokens = pm.originalSent!.tokens!
|
||||
.map((token) => PangeaToken.fromJson(token.toJson()))
|
||||
.toList();
|
||||
|
||||
// change the morphological tag in the selected token
|
||||
final tokenIndex = existingTokens
|
||||
.indexWhere((token) => token.text.offset == widget.token.text.offset);
|
||||
if (tokenIndex == -1) {
|
||||
throw Exception("Token not found in message");
|
||||
}
|
||||
existingTokens[tokenIndex].morph[widget.morphFeature] = selectedMorphTag;
|
||||
|
||||
// send a new message as an edit to original message to the server
|
||||
// including the new tokens
|
||||
await pm.room.pangeaSendTextEvent(
|
||||
pm.messageDisplayText,
|
||||
editEventId: pm.eventId,
|
||||
originalSent: pm.originalSent?.content,
|
||||
originalWritten: pm.originalWritten?.content,
|
||||
tokensSent: PangeaMessageTokens(tokens: existingTokens),
|
||||
tokensWritten: pm.originalWritten?.tokens != null
|
||||
? PangeaMessageTokens(tokens: pm.originalWritten!.tokens!)
|
||||
: null,
|
||||
choreo: pm.originalSent?.choreo,
|
||||
);
|
||||
} catch (e) {
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context).oopsSomethingWentWrong),
|
||||
);
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
data: {
|
||||
"selectedMorphTag": selectedMorphTag,
|
||||
"morphFeature": widget.morphFeature,
|
||||
"token": widget.token.toJson(),
|
||||
"pangeaMessageEvent": widget.pangeaMessageEvent.event.content,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> get allMorphTagsForEdit {
|
||||
final List<String> tags = getLabelsForMorphCategory(widget.morphFeature)
|
||||
.where((tag) => !["punct", "space", "sym", "x", "other"]
|
||||
.contains(tag.toLowerCase()))
|
||||
.toList();
|
||||
|
||||
// as long as the feature is not POS, add a nan tag
|
||||
// this will allow the user to remove the feature from the tags
|
||||
if (widget.morphFeature.toLowerCase() != "pos") {
|
||||
tags.add(L10n.of(context).constructUseNanDesc);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!editMode) {
|
||||
return GestureDetector(
|
||||
onLongPress: enterEditMode,
|
||||
onDoubleTap: enterEditMode,
|
||||
child: Text(
|
||||
getGrammarCopy(
|
||||
category: widget.morphFeature,
|
||||
lemma: widget.token.morph[widget.morphFeature],
|
||||
context: context,
|
||||
) ??
|
||||
widget.token.morph[widget.morphFeature]!,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
L10n.of(context).editMorphologicalLabel,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 170),
|
||||
child: Scrollbar(
|
||||
thumbVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: allMorphTagsForEdit.map((tag) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(2),
|
||||
padding: EdgeInsets.zero,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
border: Border.all(
|
||||
color: selectedMorphTag == tag
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.transparent,
|
||||
style: BorderStyle.solid,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 7),
|
||||
),
|
||||
backgroundColor: selectedMorphTag == tag
|
||||
? WidgetStateProperty.all<Color>(
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withAlpha(50),
|
||||
)
|
||||
: null,
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => selectedMorphTag = tag);
|
||||
},
|
||||
child: Text(
|
||||
getGrammarCopy(
|
||||
category: widget.morphFeature,
|
||||
lemma: tag,
|
||||
context: context,
|
||||
) ??
|
||||
tag,
|
||||
style: BotStyle.text(context),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
//cancel button
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
editMode = false;
|
||||
});
|
||||
},
|
||||
child: Text(L10n.of(context).cancel),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
onPressed:
|
||||
selectedMorphTag == widget.token.morph[widget.morphFeature]
|
||||
? null
|
||||
: () => showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: confirmChanges,
|
||||
),
|
||||
child: Text(L10n.of(context).saveChanges),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/enum/analytics/morph_categories_enum.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/word_zoom_activity_button.dart';
|
||||
|
||||
class MorphologicalListItem extends StatelessWidget {
|
||||
final Function(String) onPressed;
|
||||
final String morphCategory;
|
||||
final IconData icon;
|
||||
|
||||
final bool isUnlocked;
|
||||
final bool isSelected;
|
||||
|
||||
const MorphologicalListItem({
|
||||
required this.onPressed,
|
||||
required this.morphCategory,
|
||||
required this.icon,
|
||||
this.isUnlocked = true,
|
||||
this.isSelected = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WordZoomActivityButton(
|
||||
icon: Icon(icon),
|
||||
isSelected: isSelected,
|
||||
onPressed: () => onPressed(morphCategory),
|
||||
tooltip: getMorphologicalCategoryCopy(
|
||||
morphCategory,
|
||||
context,
|
||||
),
|
||||
opacity: (isSelected || !isUnlocked) ? 1 : 0.5,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/constants/morph_categories_and_labels.dart';
|
||||
import 'package:fluffychat/pangea/enum/analytics/morph_categories_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/word_zoom_activity_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/word_zoom/morphs/morphological_list_item.dart';
|
||||
|
||||
class ActivityMorph {
|
||||
final String morphFeature;
|
||||
|
|
@ -72,7 +71,7 @@ class MorphologicalListWidget extends StatelessWidget {
|
|||
children: _visibleMorphs.map((morph) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: MorphologicalActivityButton(
|
||||
child: MorphologicalListItem(
|
||||
onPressed: setMorphFeature,
|
||||
morphCategory: morph.morphFeature,
|
||||
icon: getIconForMorphFeature(morph.morphFeature),
|
||||
|
|
@ -84,35 +83,3 @@ class MorphologicalListWidget extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MorphologicalActivityButton extends StatelessWidget {
|
||||
final Function(String) onPressed;
|
||||
final String morphCategory;
|
||||
final IconData icon;
|
||||
|
||||
final bool isUnlocked;
|
||||
final bool isSelected;
|
||||
|
||||
const MorphologicalActivityButton({
|
||||
required this.onPressed,
|
||||
required this.morphCategory,
|
||||
required this.icon,
|
||||
this.isUnlocked = true,
|
||||
this.isSelected = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WordZoomActivityButton(
|
||||
icon: Icon(icon),
|
||||
isSelected: isSelected,
|
||||
onPressed: () => onPressed(morphCategory),
|
||||
tooltip: getMorphologicalCategoryCopy(
|
||||
morphCategory,
|
||||
context,
|
||||
),
|
||||
opacity: (isSelected || !isUnlocked) ? 1 : 0.5,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,14 @@
|
|||
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/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';
|
||||
|
||||
class WordZoomCenterWidget extends StatelessWidget {
|
||||
|
|
@ -22,17 +27,44 @@ class WordZoomCenterWidget extends StatelessWidget {
|
|||
super.key,
|
||||
});
|
||||
|
||||
PangeaToken get token => wordDetailsController.widget.token;
|
||||
|
||||
Widget content(BuildContext context, WordZoomSelection selectionType) {
|
||||
switch (selectionType) {
|
||||
case WordZoomSelection.morph:
|
||||
if (selectedMorphFeature == null) {
|
||||
debugger(when: kDebugMode);
|
||||
return const Text("Morphological feature is null");
|
||||
}
|
||||
return MorphologicalCenterWidget(
|
||||
token: token,
|
||||
morphFeature: selectedMorphFeature!,
|
||||
pangeaMessageEvent: wordDetailsController.widget.messageEvent,
|
||||
);
|
||||
case WordZoomSelection.lemma:
|
||||
return Text(token.lemma.text, textAlign: TextAlign.center);
|
||||
case WordZoomSelection.emoji:
|
||||
return token.getEmoji() != null
|
||||
? Text(token.getEmoji()!)
|
||||
: const Text("emoji is null");
|
||||
case WordZoomSelection.meaning:
|
||||
return LemmaMeaningWidget(
|
||||
lemma:
|
||||
token.lemma.text.isNotEmpty ? token.lemma.text : token.lemma.form,
|
||||
pos: token.pos,
|
||||
langCode:
|
||||
wordDetailsController.widget.messageEvent.messageDisplayLangCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (selectionType == null) {
|
||||
return const ToolbarContentLoadingIndicator();
|
||||
}
|
||||
|
||||
if (shouldDoActivity || locked) {
|
||||
return PracticeActivityCard(
|
||||
pangeaMessageEvent: wordDetailsController.widget.messageEvent,
|
||||
targetTokensAndActivityType: TargetTokensAndActivityType(
|
||||
tokens: [wordDetailsController.widget.token],
|
||||
tokens: [token],
|
||||
activityType: selectionType!.activityType,
|
||||
),
|
||||
overlayController: wordDetailsController.widget.overlayController,
|
||||
|
|
@ -41,20 +73,8 @@ class WordZoomCenterWidget extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
if (selectionType == WordZoomSelection.meaning) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: LemmaMeaningWidget(
|
||||
lemma: wordDetailsController.widget.token.lemma.text.isNotEmpty
|
||||
? wordDetailsController.widget.token.lemma.text
|
||||
: wordDetailsController.widget.token.lemma.form,
|
||||
pos: wordDetailsController.widget.token.pos,
|
||||
langCode: wordDetailsController
|
||||
.widget.messageEvent.messageDisplayLangCode,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (selectionType == null) {
|
||||
return const ToolbarContentLoadingIndicator();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
|
|
@ -62,13 +82,7 @@ class WordZoomCenterWidget extends StatelessWidget {
|
|||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ActivityAnswerWidget(
|
||||
token: wordDetailsController.widget.token,
|
||||
selectionType: selectionType!,
|
||||
selectedMorphFeature: selectedMorphFeature,
|
||||
),
|
||||
],
|
||||
children: [content(context, selectionType!)],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,12 @@ import 'package:fluffychat/config/app_config.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/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/grammar/get_grammar_copy.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat/tts_controller.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/emoji_practice_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/practice_activity/word_text_with_audio_button.dart';
|
||||
import 'package:fluffychat/pangea/widgets/word_zoom/lemma_widget.dart';
|
||||
import 'package:fluffychat/pangea/widgets/word_zoom/morphological_widget.dart';
|
||||
import 'package:fluffychat/pangea/widgets/word_zoom/morphs/morphological_list_widget.dart';
|
||||
import 'package:fluffychat/pangea/widgets/word_zoom/word_zoom_center_widget.dart';
|
||||
|
||||
enum WordZoomSelection {
|
||||
|
|
@ -280,41 +279,3 @@ class WordZoomWidgetState extends State<WordZoomWidget> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActivityAnswerWidget extends StatelessWidget {
|
||||
final PangeaToken token;
|
||||
final WordZoomSelection selectionType;
|
||||
final String? selectedMorphFeature;
|
||||
|
||||
const ActivityAnswerWidget({
|
||||
super.key,
|
||||
required this.token,
|
||||
required this.selectionType,
|
||||
required this.selectedMorphFeature,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (selectionType) {
|
||||
case WordZoomSelection.morph:
|
||||
if (selectedMorphFeature == null) {
|
||||
return const Text("There should be a selected morph feature");
|
||||
}
|
||||
final String morphTag = token.morph[selectedMorphFeature!];
|
||||
final copy = getGrammarCopy(
|
||||
category: selectedMorphFeature!,
|
||||
lemma: morphTag,
|
||||
context: context,
|
||||
);
|
||||
return Text(copy ?? morphTag, textAlign: TextAlign.center);
|
||||
case WordZoomSelection.lemma:
|
||||
return Text(token.lemma.text, textAlign: TextAlign.center);
|
||||
case WordZoomSelection.emoji:
|
||||
return token.getEmoji() != null
|
||||
? Text(token.getEmoji()!)
|
||||
: const Text("emoji is null");
|
||||
case WordZoomSelection.meaning:
|
||||
return const SizedBox();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue