chore: allow users to undo autocorrection of normalization errors (#2538)
This commit is contained in:
parent
b25676a58d
commit
03a5bed450
4 changed files with 212 additions and 20 deletions
|
|
@ -425,6 +425,33 @@ class Choreographer {
|
|||
}
|
||||
}
|
||||
|
||||
void onUndoReplacement(PangeaMatch match) {
|
||||
try {
|
||||
igc.igcTextData?.undoReplacement(match);
|
||||
|
||||
choreoRecord.choreoSteps.removeWhere(
|
||||
(step) => step.acceptedOrIgnoredMatch == match,
|
||||
);
|
||||
|
||||
_textController.setSystemText(
|
||||
igc.igcTextData!.originalInput,
|
||||
EditType.igc,
|
||||
);
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: e,
|
||||
s: s,
|
||||
data: {
|
||||
"igctextData": igc.igcTextData?.toJson(),
|
||||
"match": match.toJson(),
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
setState();
|
||||
}
|
||||
}
|
||||
|
||||
void acceptNormalizationMatches() {
|
||||
for (int i = 0; i < igc.igcTextData!.matches.length; i++) {
|
||||
final isNormalizationError =
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
|
@ -8,8 +9,10 @@ import 'package:collection/collection.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/models/span_data.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/autocorrect_popup.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/overlay.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_representation_event.dart';
|
||||
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
|
@ -142,6 +145,55 @@ class IGCTextData {
|
|||
}
|
||||
}
|
||||
|
||||
void undoReplacement(PangeaMatch match) async {
|
||||
if (match.match.choices == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "pangeaMatch.match.choices is null in undoReplacement",
|
||||
data: {
|
||||
"match": match.match.toJson(),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!match.match.choices!.any((c) => c.isBestCorrection)) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "pangeaMatch.match.choices has no best correction in undoReplacement",
|
||||
data: {
|
||||
"match": match.match.toJson(),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final bestCorrection =
|
||||
match.match.choices!.firstWhere((c) => c.isBestCorrection).value;
|
||||
|
||||
final String replacement = match.match.fullText.characters
|
||||
.getRange(
|
||||
match.match.offset,
|
||||
match.match.offset + match.match.length,
|
||||
)
|
||||
.toString();
|
||||
|
||||
final newStart = originalInput.characters.take(match.match.offset);
|
||||
final newEnd = originalInput.characters.skip(
|
||||
match.match.offset + bestCorrection.characters.length,
|
||||
);
|
||||
final fullText = newStart + replacement.characters + newEnd;
|
||||
originalInput = fullText.toString();
|
||||
|
||||
for (final remainingMatch in matches) {
|
||||
remainingMatch.match.fullText = originalInput;
|
||||
if (remainingMatch.match.offset > match.match.offset) {
|
||||
remainingMatch.match.offset +=
|
||||
match.match.length - bestCorrection.characters.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<int> matchIndicesByOffset(int offset) {
|
||||
final List<int> matchesForOffset = [];
|
||||
for (final (index, match) in matches.indexed) {
|
||||
|
|
@ -198,17 +250,25 @@ class IGCTextData {
|
|||
//PTODO - handle multitoken spans
|
||||
/// Returns a list of [TextSpan]s used to display the text in the input field
|
||||
/// with the appropriate styling for each error match.
|
||||
List<TextSpan> constructTokenSpan({
|
||||
ChoreoRecordStep? choreoStep,
|
||||
List<InlineSpan> constructTokenSpan({
|
||||
required List<ChoreoRecordStep> choreoSteps,
|
||||
void Function(PangeaMatch)? onUndo,
|
||||
TextStyle? defaultStyle,
|
||||
}) {
|
||||
final stepMatch = choreoStep?.acceptedOrIgnoredMatch;
|
||||
final List<PangeaMatch> textSpanMatches = List.from(matches);
|
||||
if (stepMatch != null && stepMatch.status == PangeaMatchStatus.automatic) {
|
||||
textSpanMatches.add(stepMatch);
|
||||
}
|
||||
final automaticMatches = choreoSteps
|
||||
.where(
|
||||
(step) =>
|
||||
step.acceptedOrIgnoredMatch?.status ==
|
||||
PangeaMatchStatus.automatic,
|
||||
)
|
||||
.map((step) => step.acceptedOrIgnoredMatch)
|
||||
.whereType<PangeaMatch>()
|
||||
.toList();
|
||||
|
||||
final List<TextSpan> items = [];
|
||||
final List<PangeaMatch> textSpanMatches = List.from(matches);
|
||||
textSpanMatches.addAll(automaticMatches);
|
||||
|
||||
final List<InlineSpan> items = [];
|
||||
|
||||
if (loading) {
|
||||
return [
|
||||
|
|
@ -243,19 +303,83 @@ class IGCTextData {
|
|||
// if the pointer is in a match, then add that match to items
|
||||
// and then move the pointer to the end of the match range
|
||||
final PangeaMatch match = textSpanMatches[matchIndex];
|
||||
items.add(
|
||||
getSpanItem(
|
||||
start: match.match.offset,
|
||||
end: match.match.offset + match.match.length,
|
||||
style: match.textStyle(
|
||||
matchIndex,
|
||||
_openMatchIndex,
|
||||
defaultStyle,
|
||||
),
|
||||
),
|
||||
final style = match.textStyle(
|
||||
matchIndex,
|
||||
_openMatchIndex,
|
||||
defaultStyle,
|
||||
);
|
||||
if (match.status == PangeaMatchStatus.automatic) {
|
||||
final span = originalInput.characters
|
||||
.getRange(
|
||||
match.match.offset,
|
||||
match.match.offset +
|
||||
(match.match.choices
|
||||
?.firstWhere((c) => c.isBestCorrection)
|
||||
.value
|
||||
.characters
|
||||
.length ??
|
||||
match.match.length),
|
||||
)
|
||||
.toString();
|
||||
|
||||
currentIndex = match.match.offset + match.match.length;
|
||||
final originalText = match.match.fullText.characters
|
||||
.getRange(
|
||||
match.match.offset,
|
||||
match.match.offset + match.match.length,
|
||||
)
|
||||
.toString();
|
||||
|
||||
items.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: CompositedTransformTarget(
|
||||
link: MatrixState.pAnyState
|
||||
.layerLinkAndKey("autocorrection_$matchIndex")
|
||||
.link,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return RichText(
|
||||
key: MatrixState.pAnyState
|
||||
.layerLinkAndKey("autocorrection_$matchIndex")
|
||||
.key,
|
||||
text: TextSpan(
|
||||
text: span,
|
||||
style: style,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
OverlayUtil.showOverlay(
|
||||
context: context,
|
||||
child: AutocorrectPopup(
|
||||
originalText: originalText,
|
||||
onUndo: () => onUndo?.call(match),
|
||||
),
|
||||
transformTargetId: "autocorrection_$matchIndex",
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
currentIndex = match.match.offset +
|
||||
(match.match.choices
|
||||
?.firstWhere((c) => c.isBestCorrection)
|
||||
.value
|
||||
.length ??
|
||||
match.match.length);
|
||||
} else {
|
||||
items.add(
|
||||
getSpanItem(
|
||||
start: match.match.offset,
|
||||
end: match.match.offset + match.match.length,
|
||||
style: style,
|
||||
),
|
||||
);
|
||||
currentIndex = match.match.offset + match.match.length;
|
||||
}
|
||||
} else {
|
||||
// otherwise, if the pointer is not at a match, then add all the text
|
||||
// until the next match (or, if there is not next match, the end of the
|
||||
|
|
|
|||
35
lib/pangea/choreographer/widgets/igc/autocorrect_popup.dart
Normal file
35
lib/pangea/choreographer/widgets/igc/autocorrect_popup.dart
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class AutocorrectPopup extends StatelessWidget {
|
||||
final String originalText;
|
||||
final VoidCallback onUndo;
|
||||
|
||||
const AutocorrectPopup({
|
||||
required this.originalText,
|
||||
required this.onUndo,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface.withAlpha(200),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Text(originalText),
|
||||
InkWell(
|
||||
onTap: onUndo,
|
||||
child: const Icon(Icons.replay, size: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/models/igc_text_data_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/span_card.dart';
|
||||
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
|
||||
|
|
@ -175,8 +176,13 @@ class PangeaTextController extends TextEditingController {
|
|||
style: style,
|
||||
children: [
|
||||
...choreographer.igc.igcTextData!.constructTokenSpan(
|
||||
choreoStep: choreoSteps.isNotEmpty ? choreoSteps.last : null,
|
||||
choreoSteps: choreoSteps.isNotEmpty &&
|
||||
choreoSteps.last.acceptedOrIgnoredMatch?.status ==
|
||||
PangeaMatchStatus.automatic
|
||||
? choreoSteps
|
||||
: [],
|
||||
defaultStyle: style,
|
||||
onUndo: choreographer.onUndoReplacement,
|
||||
),
|
||||
TextSpan(text: parts[1], style: style),
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue