immutable data with separate stateful models
This commit is contained in:
parent
162350d469
commit
0db2c70ef4
29 changed files with 1063 additions and 887 deletions
|
|
@ -2196,8 +2196,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
choreographer: choreographer,
|
||||
onUpdate: () async {
|
||||
await choreographer.getLanguageHelp(manual: true);
|
||||
final matches = choreographer.igc.igcTextData?.matches;
|
||||
if (matches?.isNotEmpty == true) {
|
||||
if (choreographer.igc.canShowFirstMatch) {
|
||||
choreographer.igc.showFirstMatch(context);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ import 'package:fluffychat/pages/chat/chat.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/assistance_state_enum.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/it_step.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/utils/input_paste_listener.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/utils/pangea_text_controller.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart';
|
||||
|
|
@ -90,7 +91,7 @@ class Choreographer {
|
|||
// could happen if user clicked send button multiple times in a row
|
||||
if (isFetching) return;
|
||||
|
||||
if (igc.igcTextData != null && igc.igcTextData!.matches.isNotEmpty) {
|
||||
if (igc.canShowFirstMatch) {
|
||||
igc.showFirstMatch(context);
|
||||
return;
|
||||
} else if (isRunningIT) {
|
||||
|
|
@ -223,25 +224,25 @@ class Choreographer {
|
|||
);
|
||||
}
|
||||
|
||||
void onITStart(PangeaMatch itMatch) {
|
||||
if (!itMatch.isITStart) {
|
||||
void onITStart(PangeaMatchState itMatch) {
|
||||
if (!itMatch.updatedMatch.isITStart) {
|
||||
throw Exception("this isn't an itStart match!");
|
||||
}
|
||||
choreoMode = ChoreoMode.it;
|
||||
itController.initializeIT(
|
||||
ITStartData(_textController.text, null),
|
||||
);
|
||||
itMatch.status = PangeaMatchStatus.accepted;
|
||||
|
||||
translatedText = _textController.text;
|
||||
|
||||
//PTODO - if totally in L1, save tokens, that's good stuff
|
||||
|
||||
igc.clear();
|
||||
|
||||
_textController.setSystemText("", EditType.itStart);
|
||||
|
||||
_initChoreoRecord();
|
||||
choreoRecord!.addRecord(_textController.text, match: itMatch);
|
||||
itMatch.setStatus(PangeaMatchStatus.accepted);
|
||||
choreoRecord!.addRecord(
|
||||
_textController.text,
|
||||
match: itMatch.updatedMatch,
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles any changes to the text input
|
||||
|
|
@ -317,6 +318,7 @@ class Choreographer {
|
|||
return;
|
||||
}
|
||||
|
||||
_resetDebounceTimer();
|
||||
startLoading();
|
||||
_initChoreoRecord();
|
||||
|
||||
|
|
@ -365,9 +367,8 @@ class Choreographer {
|
|||
giveInputFocus();
|
||||
}
|
||||
|
||||
Future<void> onReplacementSelect({
|
||||
required int matchIndex,
|
||||
required int choiceIndex,
|
||||
Future<void> onAcceptReplacement({
|
||||
required PangeaMatchState match,
|
||||
}) async {
|
||||
try {
|
||||
if (igc.igcTextData == null) {
|
||||
|
|
@ -375,52 +376,35 @@ class Choreographer {
|
|||
e: "onReplacementSelect with null igcTextData",
|
||||
s: StackTrace.current,
|
||||
data: {
|
||||
"matchIndex": matchIndex,
|
||||
"choiceIndex": choiceIndex,
|
||||
"match": match.toJson(),
|
||||
},
|
||||
);
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
return;
|
||||
}
|
||||
if (igc.igcTextData!.matches[matchIndex].match.choices == null) {
|
||||
if (match.updatedMatch.match.selectedChoice == null) {
|
||||
ErrorHandler.logError(
|
||||
e: "onReplacementSelect with null choices",
|
||||
e: "onReplacementSelect with null selectedChoice",
|
||||
s: StackTrace.current,
|
||||
data: {
|
||||
"igctextData": igc.igcTextData?.toJson(),
|
||||
"matchIndex": matchIndex,
|
||||
"choiceIndex": choiceIndex,
|
||||
"match": match.toJson(),
|
||||
},
|
||||
);
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
//if it's the wrong choice, return
|
||||
// if (!igc.igcTextData!.matches[matchIndex].match.choices![choiceIndex]
|
||||
// .selected) {
|
||||
// igc.igcTextData!.matches[matchIndex].match.choices![choiceIndex]
|
||||
// .selected = true;
|
||||
// setState();
|
||||
// return;
|
||||
// }
|
||||
|
||||
igc.igcTextData!.matches[matchIndex].match.choices![choiceIndex]
|
||||
.selected = true;
|
||||
|
||||
final isNormalizationError =
|
||||
igc.spanDataController.isNormalizationError(matchIndex);
|
||||
match.updatedMatch.match.isNormalizationError();
|
||||
|
||||
final match = igc.igcTextData!.matches[matchIndex].copyWith
|
||||
..status = PangeaMatchStatus.accepted;
|
||||
|
||||
igc.igcTextData!.acceptReplacement(
|
||||
matchIndex,
|
||||
choiceIndex,
|
||||
final updatedMatch = igc.igcTextData!.acceptReplacement(
|
||||
match,
|
||||
PangeaMatchStatus.accepted,
|
||||
);
|
||||
|
||||
_textController.setSystemText(
|
||||
igc.igcTextData!.originalInput,
|
||||
igc.igcTextData!.currentText,
|
||||
EditType.igc,
|
||||
);
|
||||
|
||||
|
|
@ -429,7 +413,7 @@ class Choreographer {
|
|||
_initChoreoRecord();
|
||||
choreoRecord!.addRecord(
|
||||
_textController.text,
|
||||
match: match,
|
||||
match: updatedMatch,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -442,26 +426,24 @@ class Choreographer {
|
|||
s: stack,
|
||||
data: {
|
||||
"igctextData": igc.igcTextData?.toJson(),
|
||||
"matchIndex": matchIndex,
|
||||
"choiceIndex": choiceIndex,
|
||||
"match": match.toJson(),
|
||||
},
|
||||
);
|
||||
igc.igcTextData?.matches.clear();
|
||||
igc.clear();
|
||||
} finally {
|
||||
setState();
|
||||
}
|
||||
}
|
||||
|
||||
void onUndoReplacement(PangeaMatch match) {
|
||||
void onUndoReplacement(PangeaMatchState match) {
|
||||
try {
|
||||
igc.igcTextData?.undoReplacement(match);
|
||||
|
||||
choreoRecord?.choreoSteps.removeWhere(
|
||||
(step) => step.acceptedOrIgnoredMatch == match,
|
||||
(step) => step.acceptedOrIgnoredMatch == match.updatedMatch,
|
||||
);
|
||||
|
||||
_textController.setSystemText(
|
||||
igc.igcTextData!.originalInput,
|
||||
igc.igcTextData!.currentText,
|
||||
EditType.igc,
|
||||
);
|
||||
} catch (e, s) {
|
||||
|
|
@ -480,52 +462,35 @@ class Choreographer {
|
|||
}
|
||||
|
||||
void acceptNormalizationMatches() {
|
||||
final List<int> indices = [];
|
||||
for (int i = 0; i < igc.igcTextData!.matches.length; i++) {
|
||||
final isNormalizationError =
|
||||
igc.spanDataController.isNormalizationError(i);
|
||||
if (isNormalizationError) indices.add(i);
|
||||
}
|
||||
|
||||
if (indices.isEmpty) return;
|
||||
final normalizationsMatches = igc.igcTextData!.openNormalizationMatches;
|
||||
if (normalizationsMatches.isEmpty) return;
|
||||
|
||||
_initChoreoRecord();
|
||||
final matches = igc.igcTextData!.matches
|
||||
.where(
|
||||
(match) => indices.contains(igc.igcTextData!.matches.indexOf(match)),
|
||||
)
|
||||
.toList();
|
||||
|
||||
for (final match in matches) {
|
||||
final index = igc.igcTextData!.matches.indexOf(match);
|
||||
igc.igcTextData!.acceptReplacement(
|
||||
index,
|
||||
match.match.choices!.indexWhere(
|
||||
for (final match in normalizationsMatches) {
|
||||
match.selectChoice(
|
||||
match.updatedMatch.match.choices!.indexWhere(
|
||||
(c) => c.isBestCorrection,
|
||||
),
|
||||
);
|
||||
|
||||
final newMatch = match.copyWith;
|
||||
newMatch.status = PangeaMatchStatus.automatic;
|
||||
newMatch.match.length = match.match.choices!
|
||||
.firstWhere((c) => c.isBestCorrection)
|
||||
.value
|
||||
.characters
|
||||
.length;
|
||||
final updatedMatch = igc.igcTextData!.acceptReplacement(
|
||||
match,
|
||||
PangeaMatchStatus.automatic,
|
||||
);
|
||||
|
||||
_textController.setSystemText(
|
||||
igc.igcTextData!.originalInput,
|
||||
igc.igcTextData!.currentText,
|
||||
EditType.igc,
|
||||
);
|
||||
|
||||
choreoRecord!.addRecord(
|
||||
currentText,
|
||||
match: newMatch,
|
||||
match: updatedMatch,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onIgnoreMatch({required int matchIndex}) {
|
||||
void onIgnoreMatch({required PangeaMatchState match}) {
|
||||
try {
|
||||
if (igc.igcTextData == null) {
|
||||
debugger(when: kDebugMode);
|
||||
|
|
@ -537,33 +502,23 @@ class Choreographer {
|
|||
return;
|
||||
}
|
||||
|
||||
if (matchIndex == -1) {
|
||||
debugger(when: kDebugMode);
|
||||
throw Exception("Cannot find the ignored match in igcTextData");
|
||||
}
|
||||
final updatedMatch = igc.igcTextData!.ignoreReplacement(match);
|
||||
igc.onIgnoreMatch(updatedMatch);
|
||||
|
||||
igc.onIgnoreMatch(igc.igcTextData!.matches[matchIndex]);
|
||||
igc.igcTextData!.matches[matchIndex].status = PangeaMatchStatus.ignored;
|
||||
|
||||
final isNormalizationError =
|
||||
igc.spanDataController.isNormalizationError(matchIndex);
|
||||
|
||||
if (!isNormalizationError) {
|
||||
if (!updatedMatch.match.isNormalizationError()) {
|
||||
_initChoreoRecord();
|
||||
choreoRecord!.addRecord(
|
||||
_textController.text,
|
||||
match: igc.igcTextData!.matches[matchIndex],
|
||||
match: updatedMatch,
|
||||
);
|
||||
}
|
||||
|
||||
igc.igcTextData!.matches.removeAt(matchIndex);
|
||||
} catch (err, stack) {
|
||||
debugger(when: kDebugMode);
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(
|
||||
data: {
|
||||
"igcTextData": igc.igcTextData?.toJson(),
|
||||
"matchIndex": matchIndex,
|
||||
"match": match.toJson(),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -574,7 +529,7 @@ class Choreographer {
|
|||
"igctextData": igc.igcTextData?.toJson(),
|
||||
},
|
||||
);
|
||||
igc.igcTextData?.matches.clear();
|
||||
igc.clear();
|
||||
} finally {
|
||||
setState();
|
||||
}
|
||||
|
|
@ -717,7 +672,7 @@ class Choreographer {
|
|||
return AssistanceState.noMessage;
|
||||
}
|
||||
|
||||
if ((igc.igcTextData?.matches.isNotEmpty ?? false) || isRunningIT) {
|
||||
if ((igc.igcTextData?.hasOpenMatches ?? false) || isRunningIT) {
|
||||
return AssistanceState.fetched;
|
||||
}
|
||||
|
||||
|
|
@ -756,10 +711,8 @@ class Choreographer {
|
|||
}
|
||||
|
||||
// if they have relevant matches, don't let them send
|
||||
final hasITMatches =
|
||||
igc.igcTextData!.matches.any((match) => match.isITStart);
|
||||
final hasIGCMatches =
|
||||
igc.igcTextData!.matches.any((match) => !match.isITStart);
|
||||
final hasITMatches = igc.igcTextData!.hasOpenITMatches;
|
||||
final hasIGCMatches = igc.igcTextData!.hasOpenIGCMatches;
|
||||
if ((itEnabled && hasITMatches) || (igcEnabled && hasIGCMatches)) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ import 'package:matrix/matrix.dart' hide Result;
|
|||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/span_data_controller.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/models/pangea_match_state.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/igc_repo.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/igc_request_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/span_data_repo.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/span_data_request.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/span_card.dart';
|
||||
import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
|
@ -24,11 +26,8 @@ import '../../common/utils/overlay.dart';
|
|||
class IgcController {
|
||||
Choreographer choreographer;
|
||||
IGCTextData? igcTextData;
|
||||
late SpanDataController spanDataController;
|
||||
|
||||
IgcController(this.choreographer) {
|
||||
spanDataController = SpanDataController(choreographer);
|
||||
}
|
||||
IgcController(this.choreographer);
|
||||
|
||||
Future<void> getIGCTextData() async {
|
||||
if (choreographer.currentText.isEmpty) return clear();
|
||||
|
|
@ -68,39 +67,16 @@ class IgcController {
|
|||
if (res.result!.originalInput.trim() != choreographer.currentText.trim()) {
|
||||
return;
|
||||
}
|
||||
// get ignored matches from the original igcTextData
|
||||
// if the new matches are the same as the original match
|
||||
// could possibly change the status of the new match
|
||||
// thing is the same if the text we are trying to change is the smae
|
||||
// as the new text we are trying to change (suggestion is the same)
|
||||
|
||||
// Check for duplicate or minor text changes that shouldn't trigger suggestions
|
||||
// checks for duplicate input
|
||||
|
||||
igcTextData = res.result!;
|
||||
final List<PangeaMatch> filteredMatches = List.from(igcTextData!.matches);
|
||||
for (final PangeaMatch match in igcTextData!.matches) {
|
||||
if (IgcRepo.isIgnored(match)) {
|
||||
filteredMatches.remove(match);
|
||||
}
|
||||
}
|
||||
|
||||
igcTextData!.matches = filteredMatches;
|
||||
final response = res.result!;
|
||||
igcTextData = IGCTextData(
|
||||
originalInput: response.originalInput,
|
||||
matches: response.matches,
|
||||
);
|
||||
choreographer.acceptNormalizationMatches();
|
||||
|
||||
// TODO - for each new match,
|
||||
// check if existing igcTextData has one and only one match with the same error text and correction
|
||||
// if so, keep the original match and discard the new one
|
||||
// if not, add the new match to the existing igcTextData
|
||||
|
||||
// After fetching igc data, pre-call span details for each match optimistically.
|
||||
// This will make the loading of span details faster for the user
|
||||
if (igcTextData?.matches.isNotEmpty ?? false) {
|
||||
for (int i = 0; i < igcTextData!.matches.length; i++) {
|
||||
if (!igcTextData!.matches[i].isITStart) {
|
||||
spanDataController.getSpanDetails(i);
|
||||
}
|
||||
}
|
||||
for (final match in igcTextData!.openMatches) {
|
||||
setSpanDetails(match: match);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,8 +84,12 @@ class IgcController {
|
|||
IgcRepo.ignore(match);
|
||||
}
|
||||
|
||||
bool get canShowFirstMatch {
|
||||
return igcTextData?.firstOpenMatch != null;
|
||||
}
|
||||
|
||||
void showFirstMatch(BuildContext context) {
|
||||
if (igcTextData == null || igcTextData!.matches.isEmpty) {
|
||||
if (!canShowFirstMatch) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "should not be calling showFirstMatch with this igcTextData.",
|
||||
|
|
@ -121,25 +101,20 @@ class IgcController {
|
|||
return;
|
||||
}
|
||||
|
||||
const int firstMatchIndex = 0;
|
||||
final PangeaMatch match = igcTextData!.matches[firstMatchIndex];
|
||||
|
||||
if (match.isITStart &&
|
||||
// choreographer.itAutoPlayEnabled &&
|
||||
igcTextData != null) {
|
||||
choreographer.onITStart(igcTextData!.matches[firstMatchIndex]);
|
||||
final match = igcTextData!.firstOpenMatch!;
|
||||
if (match.updatedMatch.isITStart && igcTextData != null) {
|
||||
choreographer.onITStart(match);
|
||||
return;
|
||||
}
|
||||
|
||||
choreographer.chatController.inputFocus.unfocus();
|
||||
MatrixState.pAnyState.closeAllOverlays(
|
||||
filter: RegExp(r'span_card_overlay_\d+'),
|
||||
);
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
OverlayUtil.showPositionedCard(
|
||||
overlayKey: "span_card_overlay_$firstMatchIndex",
|
||||
overlayKey:
|
||||
"span_card_overlay_${match.updatedMatch.match.offset}_${match.updatedMatch.match.length}",
|
||||
context: context,
|
||||
cardToShow: SpanCard(
|
||||
matchIndex: firstMatchIndex,
|
||||
match: match,
|
||||
choreographer: choreographer,
|
||||
),
|
||||
maxHeight: 325,
|
||||
|
|
@ -191,7 +166,7 @@ class IgcController {
|
|||
bool get hasRelevantIGCTextData {
|
||||
if (igcTextData == null) return false;
|
||||
|
||||
if (igcTextData!.originalInput != choreographer.currentText) {
|
||||
if (igcTextData!.currentText != choreographer.currentText) {
|
||||
debugPrint(
|
||||
"returning isIGCTextDataRelevant false because text has changed",
|
||||
);
|
||||
|
|
@ -202,8 +177,36 @@ class IgcController {
|
|||
|
||||
clear() {
|
||||
igcTextData = null;
|
||||
MatrixState.pAnyState.closeAllOverlays(
|
||||
filter: RegExp(r'span_card_overlay_\d+'),
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
}
|
||||
|
||||
Future<void> setSpanDetails({
|
||||
required PangeaMatchState match,
|
||||
bool force = false,
|
||||
}) async {
|
||||
final span = match.updatedMatch.match;
|
||||
if (span.isNormalizationError() && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
final response = await SpanDataRepo.get(
|
||||
choreographer.accessToken,
|
||||
request: SpanDetailsRequest(
|
||||
userL1: choreographer.l1LangCode!,
|
||||
userL2: choreographer.l2LangCode!,
|
||||
enableIGC: choreographer.igcEnabled,
|
||||
enableIT: choreographer.itEnabled,
|
||||
span: span,
|
||||
),
|
||||
);
|
||||
|
||||
if (response.isError) {
|
||||
choreographer.errorService.setError(ChoreoError(raw: response.error));
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
igcTextData?.setSpanData(match, response.result!.span);
|
||||
choreographer.setState();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ import 'package:sentry_flutter/sentry_flutter.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/interactive_translation_repo.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/it_repo.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../models/it_step.dart';
|
||||
import '../repo/custom_input_request_model.dart';
|
||||
import '../repo/it_request_model.dart';
|
||||
import '../repo/it_response_model.dart';
|
||||
import 'choreographer.dart';
|
||||
|
||||
|
|
@ -265,7 +265,7 @@ class ITController {
|
|||
}
|
||||
}
|
||||
|
||||
CustomInputRequestModel _request(String textInput) => CustomInputRequestModel(
|
||||
ITRequestModel _request(String textInput) => ITRequestModel(
|
||||
text: sourceText!,
|
||||
customInput: textInput,
|
||||
sourceLangCode: sourceLangCode,
|
||||
|
|
@ -279,7 +279,10 @@ class ITController {
|
|||
//maybe we store IT data in the same format? make a specific kind of match?
|
||||
void selectTranslation(int chosenIndex) {
|
||||
if (currentITStep == null) return;
|
||||
final itStep = ITStep(currentITStep!.continuances, chosen: chosenIndex);
|
||||
final itStep = ITStep(
|
||||
currentITStep!.continuances,
|
||||
chosen: chosenIndex,
|
||||
);
|
||||
|
||||
completedITSteps.add(itStep);
|
||||
choreographer.onITChoiceSelect(itStep);
|
||||
|
|
@ -393,7 +396,9 @@ class CurrentITStep {
|
|||
.map((e) {
|
||||
//we only want one green choice and for that to be our gold
|
||||
if (e.level == ChoreoConstants.levelThresholdForGreen) {
|
||||
e.level = ChoreoConstants.levelThresholdForYellow;
|
||||
return e.copyWith(
|
||||
level: ChoreoConstants.levelThresholdForYellow,
|
||||
);
|
||||
}
|
||||
return e;
|
||||
}),
|
||||
|
|
@ -401,7 +406,7 @@ class CurrentITStep {
|
|||
];
|
||||
continuances.shuffle();
|
||||
} else {
|
||||
continuances = responseModel.continuances;
|
||||
continuances = List<Continuance>.from(responseModel.continuances);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/span_data_repo.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/utils/text_normalization_util.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
|
||||
class SpanDataController {
|
||||
late Choreographer choreographer;
|
||||
SpanDataController(this.choreographer);
|
||||
|
||||
SpanData? _getSpan(int matchIndex) {
|
||||
if (choreographer.igc.igcTextData == null ||
|
||||
choreographer.igc.igcTextData!.matches.isEmpty ||
|
||||
matchIndex < 0 ||
|
||||
matchIndex >= choreographer.igc.igcTextData!.matches.length) {
|
||||
debugger(when: kDebugMode);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Retrieves the span data from the `igcTextData` matches at the specified `matchIndex`.
|
||||
/// Creates a `SpanDetailsRepoReqAndRes` object with the retrieved span data and other parameters.
|
||||
/// Generates a cache key based on the created `SpanDetailsRepoReqAndRes` object.
|
||||
return choreographer.igc.igcTextData!.matches[matchIndex].match;
|
||||
}
|
||||
|
||||
bool isNormalizationError(int matchIndex) {
|
||||
final span = _getSpan(matchIndex);
|
||||
if (span == null) return false;
|
||||
|
||||
final correctChoice = span.choices
|
||||
?.firstWhereOrNull(
|
||||
(c) => c.isBestCorrection,
|
||||
)
|
||||
?.value;
|
||||
|
||||
final errorSpan = span.fullText.substring(
|
||||
span.offset,
|
||||
span.offset + span.length,
|
||||
);
|
||||
|
||||
return correctChoice != null &&
|
||||
TextNormalizationUtil.normalizeString(correctChoice) ==
|
||||
TextNormalizationUtil.normalizeString(errorSpan);
|
||||
}
|
||||
|
||||
Future<void> getSpanDetails(
|
||||
int matchIndex, {
|
||||
bool force = false,
|
||||
}) async {
|
||||
final SpanData? span = _getSpan(matchIndex);
|
||||
if (span == null || (isNormalizationError(matchIndex) && !force)) return;
|
||||
final response = await SpanDataRepo.get(
|
||||
choreographer.accessToken,
|
||||
request: SpanDetailsRepoReqAndRes(
|
||||
userL1: choreographer.l1LangCode!,
|
||||
userL2: choreographer.l2LangCode!,
|
||||
enableIGC: choreographer.igcEnabled,
|
||||
enableIT: choreographer.itEnabled,
|
||||
span: span,
|
||||
),
|
||||
);
|
||||
|
||||
if (response.result != null) {
|
||||
choreographer.igc.igcTextData!.matches[matchIndex].match =
|
||||
response.result!.span;
|
||||
}
|
||||
|
||||
choreographer.setState();
|
||||
}
|
||||
}
|
||||
23
lib/pangea/choreographer/enums/pangea_match_status.dart
Normal file
23
lib/pangea/choreographer/enums/pangea_match_status.dart
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
enum PangeaMatchStatus {
|
||||
open,
|
||||
ignored,
|
||||
accepted,
|
||||
automatic,
|
||||
unknown;
|
||||
|
||||
static PangeaMatchStatus fromString(String status) {
|
||||
final String lastPart = status.toString().split('.').last;
|
||||
switch (lastPart) {
|
||||
case 'open':
|
||||
return PangeaMatchStatus.open;
|
||||
case 'ignored':
|
||||
return PangeaMatchStatus.ignored;
|
||||
case 'accepted':
|
||||
return PangeaMatchStatus.accepted;
|
||||
case 'automatic':
|
||||
return PangeaMatchStatus.automatic;
|
||||
default:
|
||||
return PangeaMatchStatus.unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,31 +4,37 @@ import 'dart:math';
|
|||
/// Remove substring of length 'length', starting at position 'offset'
|
||||
/// Then add String 'insert' at that position
|
||||
class ChoreoEdit {
|
||||
int offset = 0;
|
||||
int length = 0;
|
||||
String insert = "";
|
||||
final int offset;
|
||||
final int length;
|
||||
final String insert;
|
||||
|
||||
/// Normal constructor created from preexisting ChoreoEdit values
|
||||
ChoreoEdit({
|
||||
required this.offset,
|
||||
required this.length,
|
||||
required this.insert,
|
||||
const ChoreoEdit({
|
||||
this.offset = 0,
|
||||
this.length = 0,
|
||||
this.insert = "",
|
||||
});
|
||||
|
||||
/// Constructor that determines and saves
|
||||
/// edits differentiating originalText and editedText
|
||||
ChoreoEdit.fromText({
|
||||
factory ChoreoEdit.fromText({
|
||||
required String originalText,
|
||||
required String editedText,
|
||||
}) {
|
||||
if (originalText == editedText) {
|
||||
// No changes, return empty edit
|
||||
return;
|
||||
return const ChoreoEdit();
|
||||
}
|
||||
|
||||
offset = _firstDifference(originalText, editedText);
|
||||
length = _lastDifference(originalText, editedText) + 1 - offset;
|
||||
insert = _insertion(originalText, editedText);
|
||||
final offset = _firstDifference(originalText, editedText);
|
||||
final length =
|
||||
_lastDifference(originalText, editedText, offset) + 1 - offset;
|
||||
final insert = _insertion(originalText, editedText, offset, length);
|
||||
return ChoreoEdit(
|
||||
offset: offset,
|
||||
length: length,
|
||||
insert: insert,
|
||||
);
|
||||
}
|
||||
|
||||
factory ChoreoEdit.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -52,7 +58,7 @@ class ChoreoEdit {
|
|||
}
|
||||
|
||||
/// Find index of first character where strings differ
|
||||
int _firstDifference(String originalText, String editedText) {
|
||||
static int _firstDifference(String originalText, String editedText) {
|
||||
var i = 0;
|
||||
final minLength = min(originalText.length, editedText.length);
|
||||
while (i < minLength && originalText[i] == editedText[i]) {
|
||||
|
|
@ -63,7 +69,11 @@ class ChoreoEdit {
|
|||
|
||||
/// Starting at the end of both text versions,
|
||||
/// traverse backward until a non-matching char is found
|
||||
int _lastDifference(String originalText, String editedText) {
|
||||
static int _lastDifference(
|
||||
String originalText,
|
||||
String editedText,
|
||||
int offset,
|
||||
) {
|
||||
var i = originalText.length - 1;
|
||||
var j = editedText.length - 1;
|
||||
while (min(i, j) >= offset && originalText[i] == editedText[j]) {
|
||||
|
|
@ -77,7 +87,12 @@ class ChoreoEdit {
|
|||
/// plus the difference in string length
|
||||
/// If dif is -x and length of deleted text is x,
|
||||
/// inserted text is empty string
|
||||
String _insertion(String originalText, String editedText) {
|
||||
static String _insertion(
|
||||
String originalText,
|
||||
String editedText,
|
||||
int offset,
|
||||
int length,
|
||||
) {
|
||||
final insertLength = length + (editedText.length - originalText.length);
|
||||
return editedText.substring(offset, offset + insertLength);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/choreo_edit.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
|
||||
|
|
@ -16,14 +17,12 @@ class ChoreoRecord {
|
|||
/// ordered versions of the representation, with first being original and last
|
||||
/// being the final sent text
|
||||
/// there is not a 1-to-1 map from steps to matches
|
||||
List<ChoreoRecordStep> choreoSteps;
|
||||
|
||||
List<PangeaMatch> openMatches;
|
||||
final List<ChoreoRecordStep> choreoSteps;
|
||||
final List<PangeaMatch> openMatches;
|
||||
final String originalText;
|
||||
|
||||
final Set<String> pastedStrings = {};
|
||||
|
||||
final String originalText;
|
||||
|
||||
ChoreoRecord({
|
||||
required this.choreoSteps,
|
||||
required this.openMatches,
|
||||
|
|
@ -76,7 +75,7 @@ class ChoreoRecord {
|
|||
int length = 0;
|
||||
String insert = "";
|
||||
|
||||
final step = ChoreoRecordStep.fromJson(content);
|
||||
ChoreoRecordStep step = ChoreoRecordStep.fromJson(content);
|
||||
if (step.acceptedOrIgnoredMatch != null) {
|
||||
final SpanData? match = step.acceptedOrIgnoredMatch?.match;
|
||||
final correction = match?.bestChoice;
|
||||
|
|
@ -109,7 +108,11 @@ class ChoreoRecord {
|
|||
);
|
||||
|
||||
currentEdit = textAfter;
|
||||
step.edits = edits;
|
||||
step = ChoreoRecordStep(
|
||||
edits: edits,
|
||||
acceptedOrIgnoredMatch: step.acceptedOrIgnoredMatch,
|
||||
itStep: step.itStep,
|
||||
);
|
||||
steps.add(step);
|
||||
}
|
||||
}
|
||||
|
|
@ -255,14 +258,14 @@ class ChoreoRecordStep {
|
|||
/// will provide the current step's text
|
||||
/// Should always exist, except when using fromJSON
|
||||
/// on old version of ChoreoRecordStep
|
||||
ChoreoEdit? edits;
|
||||
final ChoreoEdit? edits;
|
||||
|
||||
/// all matches throughout edit process,
|
||||
/// including those open, accepted and ignored
|
||||
/// last step in list may contain open
|
||||
PangeaMatch? acceptedOrIgnoredMatch;
|
||||
final PangeaMatch? acceptedOrIgnoredMatch;
|
||||
|
||||
ITStep? itStep;
|
||||
final ITStep? itStep;
|
||||
|
||||
ChoreoRecordStep({
|
||||
this.edits,
|
||||
|
|
|
|||
|
|
@ -1,269 +1,63 @@
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/igc_text_state.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/utils/match_style_util.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/autocorrect_span.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
|
||||
|
||||
class IGCTextData {
|
||||
String originalInput;
|
||||
String? fullTextCorrection;
|
||||
List<PangeaMatch> matches;
|
||||
String userL1;
|
||||
String userL2;
|
||||
bool enableIT;
|
||||
bool enableIGC;
|
||||
final String _originalInput;
|
||||
final List<PangeaMatch> _matches;
|
||||
final IGCTextState _state;
|
||||
|
||||
IGCTextData({
|
||||
required this.originalInput,
|
||||
required this.fullTextCorrection,
|
||||
required this.matches,
|
||||
required this.userL1,
|
||||
required this.userL2,
|
||||
required this.enableIT,
|
||||
required this.enableIGC,
|
||||
});
|
||||
|
||||
factory IGCTextData.fromJson(Map<String, dynamic> json) {
|
||||
return IGCTextData(
|
||||
matches: json["matches"] != null
|
||||
? (json["matches"] as Iterable)
|
||||
.map<PangeaMatch>(
|
||||
(e) {
|
||||
return PangeaMatch.fromJson(e as Map<String, dynamic>);
|
||||
},
|
||||
)
|
||||
.toList()
|
||||
.cast<PangeaMatch>()
|
||||
: [],
|
||||
originalInput: json["original_input"],
|
||||
fullTextCorrection: json["full_text_correction"],
|
||||
userL1: json[ModelKey.userL1],
|
||||
userL2: json[ModelKey.userL2],
|
||||
enableIT: json["enable_it"],
|
||||
enableIGC: json["enable_igc"],
|
||||
);
|
||||
}
|
||||
required String originalInput,
|
||||
required List<PangeaMatch> matches,
|
||||
}) : _state = IGCTextState(
|
||||
currentText: originalInput,
|
||||
matches: List<PangeaMatch>.from(matches),
|
||||
),
|
||||
_originalInput = originalInput,
|
||||
_matches = matches;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"original_input": originalInput,
|
||||
"full_text_correction": fullTextCorrection,
|
||||
"matches": matches.map((e) => e.toJson()).toList(),
|
||||
ModelKey.userL1: userL1,
|
||||
ModelKey.userL2: userL2,
|
||||
"enable_it": enableIT,
|
||||
"enable_igc": enableIGC,
|
||||
"original_input": _originalInput,
|
||||
"matches": _matches.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
List<int> _matchIndicesByOffset(int offset) {
|
||||
final List<int> matchesForOffset = [];
|
||||
for (final (index, match) in matches.indexed) {
|
||||
if (match.isOffsetInMatchSpan(offset)) {
|
||||
matchesForOffset.add(index);
|
||||
}
|
||||
}
|
||||
return matchesForOffset;
|
||||
bool get hasOpenMatches => _state.hasOpenMatches;
|
||||
|
||||
bool get hasOpenITMatches => _state.hasOpenITMatches;
|
||||
|
||||
bool get hasOpenIGCMatches => _state.hasOpenIGCMatches;
|
||||
|
||||
String get currentText => _state.currentText;
|
||||
|
||||
List<PangeaMatchState> get openMatches => _state.openMatches;
|
||||
|
||||
List<PangeaMatchState> get closedMatches => _state.closedMatches;
|
||||
|
||||
PangeaMatchState? get firstOpenMatch => _state.firstOpenMatch;
|
||||
|
||||
PangeaMatchState? get openMatch => _state.openMatch;
|
||||
|
||||
PangeaMatchState? getMatchByOffset(int offset) =>
|
||||
_state.getMatchByOffset(offset);
|
||||
|
||||
List<PangeaMatchState> get openNormalizationMatches =>
|
||||
_state.openNormalizationMatches;
|
||||
|
||||
void setSpanData(PangeaMatchState match, SpanData spanData) {
|
||||
_state.setSpanData(match, spanData);
|
||||
}
|
||||
|
||||
int getTopMatchIndexForOffset(int offset) =>
|
||||
_matchIndicesByOffset(offset).firstWhereOrNull((matchIndex) {
|
||||
final match = matches[matchIndex];
|
||||
return (enableIT && (match.isITStart || match.needsTranslation)) ||
|
||||
(enableIGC && match.isGrammarMatch);
|
||||
}) ??
|
||||
-1;
|
||||
PangeaMatch acceptReplacement(
|
||||
PangeaMatchState match,
|
||||
PangeaMatchStatus status,
|
||||
) =>
|
||||
_state.acceptReplacement(match, status);
|
||||
|
||||
int? get _openMatchIndex {
|
||||
final RegExp pattern = RegExp(r'span_card_overlay_\d+');
|
||||
final String? matchingKeys =
|
||||
MatrixState.pAnyState.getMatchingOverlayKeys(pattern).firstOrNull;
|
||||
if (matchingKeys == null) return null;
|
||||
PangeaMatch ignoreReplacement(PangeaMatchState match) =>
|
||||
_state.ignoreReplacement(match);
|
||||
|
||||
final int? index = int.tryParse(matchingKeys.split("_").last);
|
||||
if (index == null ||
|
||||
matches.length <= index ||
|
||||
matches[index].status != PangeaMatchStatus.open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
InlineSpan _matchSpan(
|
||||
PangeaMatch match,
|
||||
TextStyle style,
|
||||
void Function(PangeaMatch)? onUndo,
|
||||
) {
|
||||
if (match.status == PangeaMatchStatus.automatic) {
|
||||
final span = originalInput.characters
|
||||
.getRange(
|
||||
match.match.offset,
|
||||
match.match.offset + match.match.length,
|
||||
)
|
||||
.toString();
|
||||
|
||||
final originalText = match.match.fullText.characters
|
||||
.getRange(
|
||||
match.match.offset,
|
||||
match.match.offset + match.match.length,
|
||||
)
|
||||
.toString();
|
||||
|
||||
return AutocorrectSpan(
|
||||
transformTargetId:
|
||||
"autocorrection_${match.match.offset}_${match.match.length}",
|
||||
currentText: span,
|
||||
originalText: originalText,
|
||||
onUndo: () => onUndo?.call(match),
|
||||
style: style,
|
||||
);
|
||||
} else {
|
||||
return TextSpan(
|
||||
text: originalInput.characters
|
||||
.getRange(
|
||||
match.match.offset,
|
||||
match.match.offset + match.match.length,
|
||||
)
|
||||
.toString(),
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a list of [TextSpan]s used to display the text in the input field
|
||||
/// with the appropriate styling for each error match.
|
||||
List<InlineSpan> constructTokenSpan({
|
||||
required List<ChoreoRecordStep> choreoSteps,
|
||||
void Function(PangeaMatch)? onUndo,
|
||||
TextStyle? defaultStyle,
|
||||
}) {
|
||||
final automaticMatches = choreoSteps
|
||||
.map((s) => s.acceptedOrIgnoredMatch)
|
||||
.whereType<PangeaMatch>()
|
||||
.where((m) => m.status == PangeaMatchStatus.automatic)
|
||||
.toList();
|
||||
|
||||
final textSpanMatches = [...matches, ...automaticMatches]
|
||||
..sort((a, b) => a.match.offset.compareTo(b.match.offset));
|
||||
|
||||
final spans = <InlineSpan>[];
|
||||
int cursor = 0;
|
||||
|
||||
for (final match in textSpanMatches) {
|
||||
if (cursor < match.match.offset) {
|
||||
final text = originalInput.characters
|
||||
.getRange(cursor, match.match.offset)
|
||||
.toString();
|
||||
spans.add(TextSpan(text: text, style: defaultStyle));
|
||||
}
|
||||
|
||||
final matchIndex = matches.indexWhere(
|
||||
(m) => m.match.offset == match.match.offset,
|
||||
);
|
||||
|
||||
final style = MatchStyleUtil.textStyle(
|
||||
match,
|
||||
defaultStyle,
|
||||
_openMatchIndex != null && _openMatchIndex == matchIndex,
|
||||
);
|
||||
|
||||
spans.add(_matchSpan(match, style, onUndo));
|
||||
cursor = match.match.offset + match.match.length;
|
||||
}
|
||||
|
||||
if (cursor < originalInput.characters.length) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: originalInput.characters
|
||||
.getRange(cursor, originalInput.characters.length)
|
||||
.toString(),
|
||||
style: defaultStyle,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
void acceptReplacement(
|
||||
int matchIndex,
|
||||
int choiceIndex,
|
||||
) {
|
||||
final PangeaMatch pangeaMatch = matches.removeAt(matchIndex);
|
||||
if (pangeaMatch.match.choices == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "pangeaMatch.match.choices is null in acceptReplacement",
|
||||
data: {
|
||||
"match": pangeaMatch.match.toJson(),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_runReplacement(
|
||||
pangeaMatch.match.offset,
|
||||
pangeaMatch.match.length,
|
||||
pangeaMatch.match.choices![choiceIndex].value,
|
||||
);
|
||||
}
|
||||
|
||||
void undoReplacement(PangeaMatch match) {
|
||||
final choice = match.match.choices
|
||||
?.firstWhereOrNull(
|
||||
(c) => c.isBestCorrection,
|
||||
)
|
||||
?.value;
|
||||
|
||||
if (choice == null) {
|
||||
debugger(when: kDebugMode);
|
||||
ErrorHandler.logError(
|
||||
m: "pangeaMatch.match.choices has no best correction in undoReplacement",
|
||||
data: {
|
||||
"match": match.match.toJson(),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final String replacement = match.match.fullText.characters
|
||||
.getRange(
|
||||
match.match.offset,
|
||||
match.match.offset + match.match.length,
|
||||
)
|
||||
.toString();
|
||||
|
||||
_runReplacement(
|
||||
match.match.offset,
|
||||
choice.characters.length,
|
||||
replacement,
|
||||
);
|
||||
}
|
||||
|
||||
/// Internal runner for applying a replacement to the current text.
|
||||
void _runReplacement(
|
||||
int offset,
|
||||
int length,
|
||||
String replacement,
|
||||
) {
|
||||
final newStart = originalInput.characters.take(offset);
|
||||
final newEnd = originalInput.characters.skip(offset + length);
|
||||
final fullText = newStart + replacement.characters + newEnd;
|
||||
originalInput = fullText.toString();
|
||||
|
||||
for (final match in matches) {
|
||||
match.match.fullText = originalInput;
|
||||
if (match.match.offset > offset) {
|
||||
match.match.offset += replacement.characters.length - length;
|
||||
}
|
||||
}
|
||||
}
|
||||
void undoReplacement(PangeaMatchState match) => _state.undoReplacement(match);
|
||||
}
|
||||
|
|
|
|||
204
lib/pangea/choreographer/models/igc_text_state.dart
Normal file
204
lib/pangea/choreographer/models/igc_text_state.dart
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/igc_repo.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class IGCTextState {
|
||||
String _currentText;
|
||||
final List<PangeaMatchState> _openMatches = [];
|
||||
final List<PangeaMatchState> _closedMatches = [];
|
||||
|
||||
IGCTextState({
|
||||
required String currentText,
|
||||
required List<PangeaMatch> matches,
|
||||
}) : _currentText = currentText {
|
||||
_openMatches.addAll(
|
||||
matches
|
||||
.where((match) => match.status == PangeaMatchStatus.open)
|
||||
.map((match) {
|
||||
return PangeaMatchState(
|
||||
match: match.match,
|
||||
status: match.status,
|
||||
original: match,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
_closedMatches.addAll(
|
||||
matches
|
||||
.where((match) => match.status != PangeaMatchStatus.open)
|
||||
.map((match) {
|
||||
return PangeaMatchState(
|
||||
match: match.match,
|
||||
status: match.status,
|
||||
original: match,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
_filterIgnoredMatches();
|
||||
}
|
||||
|
||||
String get currentText => _currentText;
|
||||
|
||||
List<PangeaMatchState> get openMatches => _openMatches;
|
||||
|
||||
List<PangeaMatchState> get closedMatches => _closedMatches;
|
||||
|
||||
List<PangeaMatchState> get openNormalizationMatches => _openMatches
|
||||
.where((match) => match.updatedMatch.match.isNormalizationError())
|
||||
.toList();
|
||||
|
||||
bool get hasOpenMatches => _openMatches.isNotEmpty;
|
||||
|
||||
bool get hasOpenITMatches =>
|
||||
_openMatches.any((match) => match.updatedMatch.isITStart);
|
||||
|
||||
bool get hasOpenIGCMatches =>
|
||||
_openMatches.any((match) => !match.updatedMatch.isITStart);
|
||||
|
||||
PangeaMatchState? get firstOpenMatch => _openMatches.firstOrNull;
|
||||
|
||||
PangeaMatchState? getMatchByOffset(int offset) =>
|
||||
_openMatches.firstWhereOrNull(
|
||||
(match) => match.updatedMatch.match.isOffsetInMatchSpan(offset),
|
||||
);
|
||||
|
||||
PangeaMatchState? get openMatch {
|
||||
final RegExp pattern = RegExp(r'span_card_overlay_.+');
|
||||
final String? matchingKeys =
|
||||
MatrixState.pAnyState.getMatchingOverlayKeys(pattern).firstOrNull;
|
||||
if (matchingKeys == null) return null;
|
||||
|
||||
final parts = matchingKeys.split("_");
|
||||
if (parts.length != 5) return null;
|
||||
final offset = int.tryParse(parts[3]);
|
||||
final length = int.tryParse(parts[4]);
|
||||
if (offset == null || length == null) return null;
|
||||
return _openMatches.firstWhereOrNull(
|
||||
(match) =>
|
||||
match.updatedMatch.match.offset == offset &&
|
||||
match.updatedMatch.match.length == length,
|
||||
);
|
||||
}
|
||||
|
||||
void _filterIgnoredMatches() {
|
||||
for (final match in _openMatches) {
|
||||
if (IgcRepo.isIgnored(match.updatedMatch)) {
|
||||
ignoreReplacement(match);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setSpanData(PangeaMatchState match, SpanData spanData) {
|
||||
final openMatch = _openMatches.firstWhereOrNull(
|
||||
(m) => m.originalMatch == match.originalMatch,
|
||||
);
|
||||
|
||||
match.setMatch(spanData);
|
||||
_openMatches.remove(openMatch);
|
||||
_openMatches.add(match);
|
||||
}
|
||||
|
||||
PangeaMatch acceptReplacement(
|
||||
PangeaMatchState match,
|
||||
PangeaMatchStatus status,
|
||||
) {
|
||||
final openMatch = _openMatches.firstWhereOrNull(
|
||||
(m) => m.originalMatch == match.originalMatch,
|
||||
);
|
||||
|
||||
if (match.updatedMatch.match.selectedChoice == null) {
|
||||
throw "match.match.selectedChoice is null in acceptReplacement";
|
||||
}
|
||||
|
||||
match.setStatus(status);
|
||||
_openMatches.remove(openMatch);
|
||||
_closedMatches.add(match);
|
||||
|
||||
_runReplacement(
|
||||
match.updatedMatch.match.offset,
|
||||
match.updatedMatch.match.length,
|
||||
match.updatedMatch.match.selectedChoice!.value,
|
||||
);
|
||||
|
||||
return match.updatedMatch;
|
||||
}
|
||||
|
||||
PangeaMatch ignoreReplacement(PangeaMatchState match) {
|
||||
final openMatch = _openMatches.firstWhereOrNull(
|
||||
(m) => m.originalMatch == match.originalMatch,
|
||||
);
|
||||
|
||||
match.setStatus(PangeaMatchStatus.ignored);
|
||||
_openMatches.remove(openMatch);
|
||||
_closedMatches.add(match);
|
||||
return match.updatedMatch;
|
||||
}
|
||||
|
||||
void undoReplacement(PangeaMatchState match) {
|
||||
final closedMatch = _closedMatches.firstWhereOrNull(
|
||||
(m) => m.originalMatch == match.originalMatch,
|
||||
);
|
||||
|
||||
_closedMatches.remove(closedMatch);
|
||||
|
||||
final choice = match.updatedMatch.match.selectedChoice?.value;
|
||||
|
||||
if (choice == null) {
|
||||
throw "match.match.selectedChoice is null in undoReplacement";
|
||||
}
|
||||
|
||||
final String replacement = match.originalMatch.match.fullText.characters
|
||||
.getRange(
|
||||
match.originalMatch.match.offset,
|
||||
match.originalMatch.match.offset + match.originalMatch.match.length,
|
||||
)
|
||||
.toString();
|
||||
|
||||
_runReplacement(
|
||||
match.originalMatch.match.offset,
|
||||
choice.characters.length,
|
||||
replacement,
|
||||
);
|
||||
}
|
||||
|
||||
void _runReplacement(
|
||||
int offset,
|
||||
int length,
|
||||
String replacement,
|
||||
) {
|
||||
final start = _currentText.characters.take(offset);
|
||||
final end = _currentText.characters.skip(offset + length);
|
||||
final fullText = start + replacement.characters + end;
|
||||
_currentText = fullText.toString();
|
||||
|
||||
for (int i = 0; i < _openMatches.length; i++) {
|
||||
final match = _openMatches[i].updatedMatch.match;
|
||||
final updatedMatch = match.copyWith(
|
||||
fullText: _currentText,
|
||||
offset: match.offset > offset
|
||||
? match.offset + replacement.characters.length - length
|
||||
: match.offset,
|
||||
);
|
||||
_openMatches[i].setMatch(updatedMatch);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _closedMatches.length; i++) {
|
||||
final match = _closedMatches[i].updatedMatch.match;
|
||||
final updatedMatch = match.copyWith(
|
||||
fullText: _currentText,
|
||||
offset: match.offset > offset
|
||||
? match.offset + replacement.characters.length - length
|
||||
: match.offset,
|
||||
);
|
||||
_closedMatches[i].setMatch(updatedMatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,10 +4,10 @@ import 'package:fluffychat/l10n/l10n.dart';
|
|||
import '../constants/choreo_constants.dart';
|
||||
|
||||
class ITStep {
|
||||
List<Continuance> continuances;
|
||||
int? chosen;
|
||||
String? customInput;
|
||||
bool showAlternativeTranslationOption = false;
|
||||
final List<Continuance> continuances;
|
||||
final int? chosen;
|
||||
final String? customInput;
|
||||
final bool showAlternativeTranslationOption = false;
|
||||
|
||||
ITStep(
|
||||
this.continuances, {
|
||||
|
|
@ -70,21 +70,18 @@ class ITStep {
|
|||
}
|
||||
|
||||
class Continuance {
|
||||
/// only saving this top set in a condensed json form
|
||||
double probability;
|
||||
int level;
|
||||
String text;
|
||||
// List<PangeaToken> tokens;
|
||||
final double probability;
|
||||
final int level;
|
||||
final String text;
|
||||
|
||||
/// saving this in a full json form
|
||||
String description;
|
||||
int? indexSavedByServer;
|
||||
bool wasClicked;
|
||||
bool inDictionary;
|
||||
bool hasInfo;
|
||||
bool gold;
|
||||
final String description;
|
||||
final int? indexSavedByServer;
|
||||
final bool wasClicked;
|
||||
final bool inDictionary;
|
||||
final bool hasInfo;
|
||||
final bool gold;
|
||||
|
||||
Continuance({
|
||||
const Continuance({
|
||||
required this.probability,
|
||||
required this.level,
|
||||
required this.text,
|
||||
|
|
@ -94,18 +91,9 @@ class Continuance {
|
|||
required this.inDictionary,
|
||||
required this.hasInfo,
|
||||
required this.gold,
|
||||
// required this.tokens,
|
||||
});
|
||||
|
||||
factory Continuance.fromJson(Map<String, dynamic> json) {
|
||||
// final List<PangeaToken> tokensInternal = (json[ModelKey.tokens] != null)
|
||||
// ? (json[ModelKey.tokens] as Iterable)
|
||||
// .map<PangeaToken>(
|
||||
// (e) => PangeaToken.fromJson(e as Map<String, dynamic>),
|
||||
// )
|
||||
// .toList()
|
||||
// .cast<PangeaToken>()
|
||||
// : [];
|
||||
return Continuance(
|
||||
probability: json['probability'].toDouble(),
|
||||
level: json['level'],
|
||||
|
|
@ -116,7 +104,6 @@ class Continuance {
|
|||
wasClicked: json['clkd'] ?? false,
|
||||
hasInfo: json['has_info'] ?? false,
|
||||
gold: json['gold'] ?? false,
|
||||
// tokens: tokensInternal,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +125,30 @@ class Continuance {
|
|||
return data;
|
||||
}
|
||||
|
||||
Continuance copyWith({
|
||||
double? probability,
|
||||
int? level,
|
||||
String? text,
|
||||
String? description,
|
||||
int? indexSavedByServer,
|
||||
bool? wasClicked,
|
||||
bool? inDictionary,
|
||||
bool? hasInfo,
|
||||
bool? gold,
|
||||
}) {
|
||||
return Continuance(
|
||||
probability: probability ?? this.probability,
|
||||
level: level ?? this.level,
|
||||
text: text ?? this.text,
|
||||
description: description ?? this.description,
|
||||
indexSavedByServer: indexSavedByServer ?? this.indexSavedByServer,
|
||||
wasClicked: wasClicked ?? this.wasClicked,
|
||||
inDictionary: inDictionary ?? this.inDictionary,
|
||||
hasInfo: hasInfo ?? this.hasInfo,
|
||||
gold: gold ?? this.gold,
|
||||
);
|
||||
}
|
||||
|
||||
Color? get color {
|
||||
if (!wasClicked) return null;
|
||||
switch (level) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
|
||||
class LanguageDetection {
|
||||
String langCode;
|
||||
double confidence;
|
||||
final String langCode;
|
||||
final double confidence;
|
||||
|
||||
LanguageDetection({
|
||||
const LanguageDetection({
|
||||
required this.langCode,
|
||||
required this.confidence,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,40 +2,17 @@ import 'dart:developer';
|
|||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/span_data_type.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import '../constants/match_rule_ids.dart';
|
||||
import 'span_data.dart';
|
||||
|
||||
enum PangeaMatchStatus {
|
||||
open,
|
||||
ignored,
|
||||
accepted,
|
||||
automatic,
|
||||
unknown;
|
||||
|
||||
static PangeaMatchStatus fromString(String status) {
|
||||
final String lastPart = status.toString().split('.').last;
|
||||
switch (lastPart) {
|
||||
case 'open':
|
||||
return PangeaMatchStatus.open;
|
||||
case 'ignored':
|
||||
return PangeaMatchStatus.ignored;
|
||||
case 'accepted':
|
||||
return PangeaMatchStatus.accepted;
|
||||
case 'automatic':
|
||||
return PangeaMatchStatus.automatic;
|
||||
default:
|
||||
return PangeaMatchStatus.unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PangeaMatch {
|
||||
SpanData match;
|
||||
PangeaMatchStatus status;
|
||||
final SpanData match;
|
||||
final PangeaMatchStatus status;
|
||||
|
||||
PangeaMatch({
|
||||
const PangeaMatch({
|
||||
required this.match,
|
||||
required this.status,
|
||||
});
|
||||
|
|
@ -96,8 +73,15 @@ class PangeaMatch {
|
|||
return match.fullText.substring(beginning, end);
|
||||
}
|
||||
|
||||
bool isOffsetInMatchSpan(int offset) =>
|
||||
offset >= match.offset && offset < match.offset + match.length;
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! PangeaMatch) return false;
|
||||
return other.match == match && other.status == status;
|
||||
}
|
||||
|
||||
PangeaMatch get copyWith => PangeaMatch.fromJson(toJson());
|
||||
@override
|
||||
int get hashCode {
|
||||
return match.hashCode ^ status.hashCode;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
49
lib/pangea/choreographer/models/pangea_match_state.dart
Normal file
49
lib/pangea/choreographer/models/pangea_match_state.dart
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
|
||||
|
||||
class PangeaMatchState {
|
||||
final PangeaMatch _original;
|
||||
SpanData _match;
|
||||
PangeaMatchStatus _status;
|
||||
|
||||
PangeaMatchState({
|
||||
required PangeaMatch original,
|
||||
required SpanData match,
|
||||
required PangeaMatchStatus status,
|
||||
}) : _original = original,
|
||||
_match = match,
|
||||
_status = status;
|
||||
|
||||
PangeaMatch get originalMatch => _original;
|
||||
|
||||
PangeaMatch get updatedMatch => PangeaMatch(
|
||||
match: _match,
|
||||
status: _status,
|
||||
);
|
||||
|
||||
void setStatus(PangeaMatchStatus status) {
|
||||
_status = status;
|
||||
}
|
||||
|
||||
void setMatch(SpanData match) {
|
||||
_match = match;
|
||||
}
|
||||
|
||||
void selectChoice(int index) {
|
||||
final choices = List<SpanChoice>.from(_match.choices ?? []);
|
||||
choices[index] = choices[index].copyWith(
|
||||
selected: true,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
setMatch(_match.copyWith(choices: choices));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'originalMatch': _original.toJson(),
|
||||
'match': _match.toJson(),
|
||||
'status': _status.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -8,10 +8,20 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/utils/text_normalization_util.dart';
|
||||
import '../enums/span_choice_type.dart';
|
||||
import '../enums/span_data_type.dart';
|
||||
|
||||
class SpanData {
|
||||
final String? message;
|
||||
final String? shortMessage;
|
||||
final List<SpanChoice>? choices;
|
||||
final int offset;
|
||||
final int length;
|
||||
final String fullText;
|
||||
final SpanDataType type;
|
||||
final Rule? rule;
|
||||
|
||||
SpanData({
|
||||
required this.message,
|
||||
required this.shortMessage,
|
||||
|
|
@ -23,6 +33,28 @@ class SpanData {
|
|||
required this.rule,
|
||||
});
|
||||
|
||||
SpanData copyWith({
|
||||
String? message,
|
||||
String? shortMessage,
|
||||
List<SpanChoice>? choices,
|
||||
int? offset,
|
||||
int? length,
|
||||
String? fullText,
|
||||
SpanDataType? type,
|
||||
Rule? rule,
|
||||
}) {
|
||||
return SpanData(
|
||||
message: message ?? this.message,
|
||||
shortMessage: shortMessage ?? this.shortMessage,
|
||||
choices: choices ?? this.choices,
|
||||
offset: offset ?? this.offset,
|
||||
length: length ?? this.length,
|
||||
fullText: fullText ?? this.fullText,
|
||||
type: type ?? this.type,
|
||||
rule: rule ?? this.rule,
|
||||
);
|
||||
}
|
||||
|
||||
factory SpanData.fromJson(Map<String, dynamic> json) {
|
||||
final Iterable? choices = json['choices'] ?? json['replacements'];
|
||||
return SpanData(
|
||||
|
|
@ -44,15 +76,6 @@ class SpanData {
|
|||
);
|
||||
}
|
||||
|
||||
String? message;
|
||||
String? shortMessage;
|
||||
List<SpanChoice>? choices;
|
||||
int offset;
|
||||
int length;
|
||||
String fullText;
|
||||
SpanDataType type;
|
||||
Rule? rule;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'message': message,
|
||||
'short_message': shortMessage,
|
||||
|
|
@ -66,20 +89,107 @@ class SpanData {
|
|||
'rule': rule?.toJson(),
|
||||
};
|
||||
|
||||
bool isOffsetInMatchSpan(int offset) =>
|
||||
offset >= this.offset && offset < this.offset + length;
|
||||
|
||||
SpanChoice? get bestChoice {
|
||||
return choices?.firstWhereOrNull(
|
||||
(choice) => choice.isBestCorrection,
|
||||
);
|
||||
}
|
||||
|
||||
int get selectedChoiceIndex {
|
||||
if (choices == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// if user ever selected the correct choice, automatically select it
|
||||
final selectedCorrectIndex = choices!.indexWhere((choice) {
|
||||
return choice.selected && choice.isBestCorrection;
|
||||
});
|
||||
|
||||
if (selectedCorrectIndex != -1) {
|
||||
return selectedCorrectIndex;
|
||||
}
|
||||
|
||||
SpanChoice? mostRecent;
|
||||
for (int i = 0; i < choices!.length; i++) {
|
||||
final choice = choices![i];
|
||||
if (choice.timestamp != null &&
|
||||
(mostRecent == null ||
|
||||
choice.timestamp!.isAfter(mostRecent.timestamp!))) {
|
||||
mostRecent = choice;
|
||||
}
|
||||
}
|
||||
return mostRecent != null ? choices!.indexOf(mostRecent) : -1;
|
||||
}
|
||||
|
||||
SpanChoice? get selectedChoice {
|
||||
final index = selectedChoiceIndex;
|
||||
if (index == -1) {
|
||||
return null;
|
||||
}
|
||||
return choices![index];
|
||||
}
|
||||
|
||||
bool isNormalizationError() {
|
||||
final correctChoice = choices
|
||||
?.firstWhereOrNull(
|
||||
(c) => c.isBestCorrection,
|
||||
)
|
||||
?.value;
|
||||
|
||||
final errorSpan = fullText.characters.skip(offset).take(length).toString();
|
||||
|
||||
return correctChoice != null &&
|
||||
TextNormalizationUtil.normalizeString(correctChoice) ==
|
||||
TextNormalizationUtil.normalizeString(errorSpan);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! SpanData) return false;
|
||||
if (other.message != message) return false;
|
||||
if (other.shortMessage != shortMessage) return false;
|
||||
if (other.offset != offset) return false;
|
||||
if (other.length != length) return false;
|
||||
if (other.fullText != fullText) return false;
|
||||
if (other.type != type) return false;
|
||||
if (other.rule != rule) return false;
|
||||
if (const ListEquality().equals(
|
||||
other.choices?.sorted((a, b) => b.value.compareTo(a.value)),
|
||||
choices?.sorted((a, b) => b.value.compareTo(a.value)),
|
||||
) ==
|
||||
false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return message.hashCode ^
|
||||
shortMessage.hashCode ^
|
||||
Object.hashAll(
|
||||
(choices ?? [])
|
||||
.sorted((a, b) => b.value.compareTo(a.value))
|
||||
.map((choice) => choice.hashCode),
|
||||
) ^
|
||||
offset.hashCode ^
|
||||
length.hashCode ^
|
||||
fullText.hashCode ^
|
||||
type.hashCode ^
|
||||
rule.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
class SpanChoice {
|
||||
String value;
|
||||
SpanChoiceType type;
|
||||
bool selected;
|
||||
String? feedback;
|
||||
DateTime? timestamp;
|
||||
// List<PangeaToken> tokens;
|
||||
final String value;
|
||||
final SpanChoiceType type;
|
||||
final bool selected;
|
||||
final String? feedback;
|
||||
final DateTime? timestamp;
|
||||
|
||||
SpanChoice({
|
||||
required this.value,
|
||||
|
|
@ -87,18 +197,25 @@ class SpanChoice {
|
|||
this.feedback,
|
||||
this.selected = false,
|
||||
this.timestamp,
|
||||
// this.tokens = const [],
|
||||
});
|
||||
|
||||
SpanChoice copyWith({
|
||||
String? value,
|
||||
SpanChoiceType? type,
|
||||
String? feedback,
|
||||
bool? selected,
|
||||
DateTime? timestamp,
|
||||
}) {
|
||||
return SpanChoice(
|
||||
value: value ?? this.value,
|
||||
type: type ?? this.type,
|
||||
feedback: feedback ?? this.feedback,
|
||||
selected: selected ?? this.selected,
|
||||
timestamp: timestamp ?? this.timestamp,
|
||||
);
|
||||
}
|
||||
|
||||
factory SpanChoice.fromJson(Map<String, dynamic> json) {
|
||||
// final List<PangeaToken> tokensInternal = (json[ModelKey.tokens] != null)
|
||||
// ? (json[ModelKey.tokens] as Iterable)
|
||||
// .map<PangeaToken>(
|
||||
// (e) => PangeaToken.fromJson(e as Map<String, dynamic>),
|
||||
// )
|
||||
// .toList()
|
||||
// .cast<PangeaToken>()
|
||||
// : [];
|
||||
return SpanChoice(
|
||||
value: json['value'] as String,
|
||||
type: json['type'] != null
|
||||
|
|
@ -111,7 +228,6 @@ class SpanChoice {
|
|||
selected: json['selected'] ?? false,
|
||||
timestamp:
|
||||
json['timestamp'] != null ? DateTime.parse(json['timestamp']) : null,
|
||||
// tokens: tokensInternal,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +237,6 @@ class SpanChoice {
|
|||
'selected': selected,
|
||||
'feedback': feedback,
|
||||
'timestamp': timestamp?.toIso8601String(),
|
||||
// 'tokens': tokens.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
String feedbackToDisplay(BuildContext context) {
|
||||
|
|
@ -147,38 +262,51 @@ class SpanChoice {
|
|||
other.type.toString() == type.toString() &&
|
||||
other.selected == selected &&
|
||||
other.feedback == feedback &&
|
||||
other.timestamp?.toIso8601String() == timestamp?.toIso8601String();
|
||||
other.timestamp == timestamp;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hashAll([
|
||||
value.hashCode,
|
||||
type.toString().hashCode,
|
||||
selected.hashCode,
|
||||
feedback.hashCode,
|
||||
timestamp?.toIso8601String().hashCode,
|
||||
]);
|
||||
return value.hashCode ^
|
||||
type.hashCode ^
|
||||
selected.hashCode ^
|
||||
feedback.hashCode ^
|
||||
timestamp.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
class Rule {
|
||||
Rule({
|
||||
final String id;
|
||||
|
||||
const Rule({
|
||||
required this.id,
|
||||
});
|
||||
|
||||
factory Rule.fromJson(Map<String, dynamic> json) => Rule(
|
||||
id: json['id'] as String,
|
||||
);
|
||||
|
||||
String id;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
};
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! Rule) return false;
|
||||
return other.id == id;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
class SpanDataType {
|
||||
SpanDataType({
|
||||
final SpanDataTypeEnum typeName;
|
||||
|
||||
const SpanDataType({
|
||||
required this.typeName,
|
||||
});
|
||||
|
||||
|
|
@ -193,9 +321,20 @@ class SpanDataType {
|
|||
: SpanDataTypeEnum.correction,
|
||||
);
|
||||
}
|
||||
SpanDataTypeEnum typeName;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'type_name': typeName.name,
|
||||
};
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! SpanDataType) return false;
|
||||
return other.typeName == typeName;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return typeName.hashCode;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ import 'package:http/http.dart';
|
|||
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/igc_request_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/igc_response_model.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import '../../common/network/requests.dart';
|
||||
import '../../common/network/urls.dart';
|
||||
import '../models/igc_text_data_model.dart';
|
||||
|
||||
class _IgcCacheItem {
|
||||
final Future<IGCTextData> data;
|
||||
final Future<IGCResponseModel> data;
|
||||
final DateTime timestamp;
|
||||
|
||||
const _IgcCacheItem({
|
||||
|
|
@ -52,7 +52,7 @@ class IgcRepo {
|
|||
static final Map<String, _IgnoredMatchCacheItem> _ignoredMatchCache = {};
|
||||
static const Duration _cacheDuration = Duration(minutes: 10);
|
||||
|
||||
static Future<Result<IGCTextData>> get(
|
||||
static Future<Result<IGCResponseModel>> get(
|
||||
String? accessToken,
|
||||
IGCRequestModel igcRequest,
|
||||
) {
|
||||
|
|
@ -69,7 +69,7 @@ class IgcRepo {
|
|||
return _getResult(igcRequest, future);
|
||||
}
|
||||
|
||||
static Future<IGCTextData> _fetch(
|
||||
static Future<IGCResponseModel> _fetch(
|
||||
String? accessToken, {
|
||||
required IGCRequestModel igcRequest,
|
||||
}) async {
|
||||
|
|
@ -91,12 +91,12 @@ class IgcRepo {
|
|||
final Map<String, dynamic> json =
|
||||
jsonDecode(utf8.decode(res.bodyBytes).toString());
|
||||
|
||||
return IGCTextData.fromJson(json);
|
||||
return IGCResponseModel.fromJson(json);
|
||||
}
|
||||
|
||||
static Future<Result<IGCTextData>> _getResult(
|
||||
static Future<Result<IGCResponseModel>> _getResult(
|
||||
IGCRequestModel request,
|
||||
Future<IGCTextData> future,
|
||||
Future<IGCResponseModel> future,
|
||||
) async {
|
||||
try {
|
||||
final res = await future;
|
||||
|
|
@ -112,7 +112,7 @@ class IgcRepo {
|
|||
}
|
||||
}
|
||||
|
||||
static Future<IGCTextData>? _getCached(
|
||||
static Future<IGCResponseModel>? _getCached(
|
||||
IGCRequestModel request,
|
||||
) {
|
||||
final cacheKeys = [..._igcCache.keys];
|
||||
|
|
@ -129,7 +129,7 @@ class IgcRepo {
|
|||
|
||||
static void _setCached(
|
||||
IGCRequestModel request,
|
||||
Future<IGCTextData> response,
|
||||
Future<IGCResponseModel> response,
|
||||
) =>
|
||||
_igcCache[request.hashCode.toString()] = _IgcCacheItem(
|
||||
data: response,
|
||||
|
|
|
|||
53
lib/pangea/choreographer/repo/igc_response_model.dart
Normal file
53
lib/pangea/choreographer/repo/igc_response_model.dart
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
|
||||
class IGCResponseModel {
|
||||
final String originalInput;
|
||||
final String? fullTextCorrection;
|
||||
final List<PangeaMatch> matches;
|
||||
final String userL1;
|
||||
final String userL2;
|
||||
final bool enableIT;
|
||||
final bool enableIGC;
|
||||
|
||||
IGCResponseModel({
|
||||
required this.originalInput,
|
||||
required this.fullTextCorrection,
|
||||
required this.matches,
|
||||
required this.userL1,
|
||||
required this.userL2,
|
||||
required this.enableIT,
|
||||
required this.enableIGC,
|
||||
});
|
||||
|
||||
factory IGCResponseModel.fromJson(Map<String, dynamic> json) {
|
||||
return IGCResponseModel(
|
||||
matches: json["matches"] != null
|
||||
? (json["matches"] as Iterable)
|
||||
.map<PangeaMatch>(
|
||||
(e) {
|
||||
return PangeaMatch.fromJson(e as Map<String, dynamic>);
|
||||
},
|
||||
)
|
||||
.toList()
|
||||
.cast<PangeaMatch>()
|
||||
: [],
|
||||
originalInput: json["original_input"],
|
||||
fullTextCorrection: json["full_text_correction"],
|
||||
userL1: json[ModelKey.userL1],
|
||||
userL2: json[ModelKey.userL2],
|
||||
enableIT: json["enable_it"],
|
||||
enableIGC: json["enable_igc"],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"original_input": originalInput,
|
||||
"full_text_correction": fullTextCorrection,
|
||||
"matches": matches.map((e) => e.toJson()).toList(),
|
||||
ModelKey.userL1: userL1,
|
||||
ModelKey.userL2: userL2,
|
||||
"enable_it": enableIT,
|
||||
"enable_igc": enableIGC,
|
||||
};
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
|||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../common/network/requests.dart';
|
||||
import '../../common/network/urls.dart';
|
||||
import 'custom_input_request_model.dart';
|
||||
import 'it_request_model.dart';
|
||||
import 'it_response_model.dart';
|
||||
|
||||
class _ITCacheItem {
|
||||
|
|
@ -26,7 +26,7 @@ class ITRepo {
|
|||
static const Duration _cacheDuration = Duration(minutes: 10);
|
||||
|
||||
static Future<Result<ITResponseModel>> get(
|
||||
CustomInputRequestModel request,
|
||||
ITRequestModel request,
|
||||
) {
|
||||
final cached = _getCached(request);
|
||||
if (cached != null) {
|
||||
|
|
@ -39,7 +39,7 @@ class ITRepo {
|
|||
}
|
||||
|
||||
static Future<ITResponseModel> _fetch(
|
||||
CustomInputRequestModel request,
|
||||
ITRequestModel request,
|
||||
) async {
|
||||
final Requests req = Requests(
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
|
|
@ -57,7 +57,7 @@ class ITRepo {
|
|||
}
|
||||
|
||||
static Future<Result<ITResponseModel>> _getResult(
|
||||
CustomInputRequestModel request,
|
||||
ITRequestModel request,
|
||||
Future<ITResponseModel> future,
|
||||
) async {
|
||||
try {
|
||||
|
|
@ -75,7 +75,7 @@ class ITRepo {
|
|||
}
|
||||
|
||||
static Future<ITResponseModel>? _getCached(
|
||||
CustomInputRequestModel request,
|
||||
ITRequestModel request,
|
||||
) {
|
||||
final cacheKeys = [..._cache.keys];
|
||||
for (final key in cacheKeys) {
|
||||
|
|
@ -87,7 +87,7 @@ class ITRepo {
|
|||
}
|
||||
|
||||
static void _setCached(
|
||||
CustomInputRequestModel request,
|
||||
ITRequestModel request,
|
||||
Future<ITResponseModel> response,
|
||||
) {
|
||||
_cache[request.hashCode.toString()] = _ITCacheItem(
|
||||
|
|
@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/models/it_step.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
|
||||
class CustomInputRequestModel {
|
||||
class ITRequestModel {
|
||||
final String text;
|
||||
final String customInput;
|
||||
final String sourceLangCode;
|
||||
|
|
@ -14,7 +14,7 @@ class CustomInputRequestModel {
|
|||
final String? goldTranslation;
|
||||
final List<Continuance>? goldContinuances;
|
||||
|
||||
const CustomInputRequestModel({
|
||||
const ITRequestModel({
|
||||
required this.text,
|
||||
required this.customInput,
|
||||
required this.sourceLangCode,
|
||||
|
|
@ -25,7 +25,7 @@ class CustomInputRequestModel {
|
|||
required this.goldContinuances,
|
||||
});
|
||||
|
||||
factory CustomInputRequestModel.fromJson(json) => CustomInputRequestModel(
|
||||
factory ITRequestModel.fromJson(json) => ITRequestModel(
|
||||
text: json['text'],
|
||||
customInput: json['custom_input'],
|
||||
sourceLangCode: json[ModelKey.srcLang],
|
||||
|
|
@ -34,7 +34,7 @@ class CustomInputRequestModel {
|
|||
roomId: json['room_id'],
|
||||
goldTranslation: json['gold_translation'],
|
||||
goldContinuances: json['gold_continuances'] != null
|
||||
? List.from(json['gold_continuances'])
|
||||
? (json['gold_continuances'])
|
||||
.map((e) => Continuance.fromJson(e))
|
||||
.toList()
|
||||
: null,
|
||||
|
|
@ -57,7 +57,7 @@ class CustomInputRequestModel {
|
|||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is CustomInputRequestModel &&
|
||||
return other is ITRequestModel &&
|
||||
other.text == text &&
|
||||
other.customInput == customInput &&
|
||||
other.sourceLangCode == sourceLangCode &&
|
||||
|
|
@ -1,18 +1,17 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/span_data_request.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/repo/span_data_response.dart';
|
||||
import 'package:fluffychat/pangea/common/config/environment.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import '../../common/constants/model_keys.dart';
|
||||
import '../../common/network/requests.dart';
|
||||
import '../../common/network/urls.dart';
|
||||
|
||||
class _SpanDetailsCacheItem {
|
||||
final Future<SpanDetailsRepoReqAndRes> data;
|
||||
final Future<SpanDetailsResponse> data;
|
||||
final DateTime timestamp;
|
||||
|
||||
const _SpanDetailsCacheItem({
|
||||
|
|
@ -25,9 +24,9 @@ class SpanDataRepo {
|
|||
static final Map<String, _SpanDetailsCacheItem> _cache = {};
|
||||
static const Duration _cacheDuration = Duration(minutes: 10);
|
||||
|
||||
static Future<Result<SpanDetailsRepoReqAndRes>> get(
|
||||
static Future<Result<SpanDetailsResponse>> get(
|
||||
String? accessToken, {
|
||||
required SpanDetailsRepoReqAndRes request,
|
||||
required SpanDetailsRequest request,
|
||||
}) async {
|
||||
final cached = _getCached(request);
|
||||
if (cached != null) {
|
||||
|
|
@ -42,28 +41,32 @@ class SpanDataRepo {
|
|||
return _getResult(request, future);
|
||||
}
|
||||
|
||||
static Future<SpanDetailsRepoReqAndRes> _fetch(
|
||||
static Future<SpanDetailsResponse> _fetch(
|
||||
String? accessToken, {
|
||||
required SpanDetailsRepoReqAndRes request,
|
||||
required SpanDetailsRequest request,
|
||||
}) async {
|
||||
final Requests req = Requests(
|
||||
accessToken: accessToken,
|
||||
choreoApiKey: Environment.choreoApiKey,
|
||||
);
|
||||
|
||||
final Response res = await req.post(
|
||||
url: PApiUrls.spanDetails,
|
||||
body: request.toJson(),
|
||||
);
|
||||
|
||||
final Map<String, dynamic> json =
|
||||
jsonDecode(utf8.decode(res.bodyBytes).toString());
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception('Failed to load span details');
|
||||
}
|
||||
|
||||
return SpanDetailsRepoReqAndRes.fromJson(json);
|
||||
return SpanDetailsResponse.fromJson(
|
||||
jsonDecode(utf8.decode(res.bodyBytes)),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Result<SpanDetailsRepoReqAndRes>> _getResult(
|
||||
SpanDetailsRepoReqAndRes request,
|
||||
Future<SpanDetailsRepoReqAndRes> future,
|
||||
static Future<Result<SpanDetailsResponse>> _getResult(
|
||||
SpanDetailsRequest request,
|
||||
Future<SpanDetailsResponse> future,
|
||||
) async {
|
||||
try {
|
||||
final res = await future;
|
||||
|
|
@ -79,8 +82,8 @@ class SpanDataRepo {
|
|||
}
|
||||
}
|
||||
|
||||
static Future<SpanDetailsRepoReqAndRes>? _getCached(
|
||||
SpanDetailsRepoReqAndRes request,
|
||||
static Future<SpanDetailsResponse>? _getCached(
|
||||
SpanDetailsRequest request,
|
||||
) {
|
||||
final cacheKeys = [..._cache.keys];
|
||||
for (final key in cacheKeys) {
|
||||
|
|
@ -92,8 +95,8 @@ class SpanDataRepo {
|
|||
}
|
||||
|
||||
static void _setCached(
|
||||
SpanDetailsRepoReqAndRes request,
|
||||
Future<SpanDetailsRepoReqAndRes> response,
|
||||
SpanDetailsRequest request,
|
||||
Future<SpanDetailsResponse> response,
|
||||
) {
|
||||
_cache[request.hashCode.toString()] = _SpanDetailsCacheItem(
|
||||
data: response,
|
||||
|
|
@ -101,75 +104,3 @@ class SpanDataRepo {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SpanDetailsRepoReqAndRes {
|
||||
String userL1;
|
||||
String userL2;
|
||||
bool enableIT;
|
||||
bool enableIGC;
|
||||
SpanData span;
|
||||
|
||||
SpanDetailsRepoReqAndRes({
|
||||
required this.userL1,
|
||||
required this.userL2,
|
||||
required this.enableIGC,
|
||||
required this.enableIT,
|
||||
required this.span,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
ModelKey.userL1: userL1,
|
||||
ModelKey.userL2: userL2,
|
||||
"enable_it": enableIT,
|
||||
"enable_igc": enableIGC,
|
||||
'span': span.toJson(),
|
||||
};
|
||||
|
||||
factory SpanDetailsRepoReqAndRes.fromJson(Map<String, dynamic> json) =>
|
||||
SpanDetailsRepoReqAndRes(
|
||||
userL1: json['user_l1'] as String,
|
||||
userL2: json['user_l2'] as String,
|
||||
enableIT: json['enable_it'] as bool,
|
||||
enableIGC: json['enable_igc'] as bool,
|
||||
span: SpanData.fromJson(json['span']),
|
||||
);
|
||||
|
||||
/// Overrides the equality operator to compare two [SpanDetailsRepoReqAndRes] objects.
|
||||
/// Returns true if the objects are identical or have the same property
|
||||
/// values (based on the results of the toJson function), false otherwise.
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! SpanDetailsRepoReqAndRes) return false;
|
||||
if (other.userL1 != userL1) return false;
|
||||
if (other.userL2 != userL2) return false;
|
||||
if (other.enableIT != enableIT) return false;
|
||||
if (other.enableIGC != enableIGC) return false;
|
||||
if (const ListEquality().equals(
|
||||
other.span.choices?.sorted((a, b) => b.value.compareTo(a.value)),
|
||||
span.choices?.sorted((a, b) => b.value.compareTo(a.value)),
|
||||
) ==
|
||||
false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Overrides the hashCode getter to generate a hash code for the [SpanDetailsRepoReqAndRes] object.
|
||||
/// Used as keys in response cache in igc_controller.
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hashAll([
|
||||
userL1.hashCode,
|
||||
userL2.hashCode,
|
||||
enableIT.hashCode,
|
||||
enableIGC.hashCode,
|
||||
if (span.choices != null)
|
||||
Object.hashAll(
|
||||
span.choices!
|
||||
.sorted((a, b) => b.value.compareTo(a.value))
|
||||
.map((choice) => choice.hashCode),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
47
lib/pangea/choreographer/repo/span_data_request.dart
Normal file
47
lib/pangea/choreographer/repo/span_data_request.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
|
||||
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
|
||||
|
||||
class SpanDetailsRequest {
|
||||
final String userL1;
|
||||
final String userL2;
|
||||
final bool enableIT;
|
||||
final bool enableIGC;
|
||||
final SpanData span;
|
||||
|
||||
const SpanDetailsRequest({
|
||||
required this.userL1,
|
||||
required this.userL2,
|
||||
required this.enableIGC,
|
||||
required this.enableIT,
|
||||
required this.span,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
ModelKey.userL1: userL1,
|
||||
ModelKey.userL2: userL2,
|
||||
"enable_it": enableIT,
|
||||
"enable_igc": enableIGC,
|
||||
'span': span.toJson(),
|
||||
};
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other is! SpanDetailsRequest) return false;
|
||||
if (other.userL1 != userL1) return false;
|
||||
if (other.userL2 != userL2) return false;
|
||||
if (other.enableIT != enableIT) return false;
|
||||
if (other.enableIGC != enableIGC) return false;
|
||||
if (other.span != span) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return userL1.hashCode ^
|
||||
userL2.hashCode ^
|
||||
enableIT.hashCode ^
|
||||
enableIGC.hashCode ^
|
||||
span.hashCode;
|
||||
}
|
||||
}
|
||||
26
lib/pangea/choreographer/repo/span_data_response.dart
Normal file
26
lib/pangea/choreographer/repo/span_data_response.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
|
||||
|
||||
class SpanDetailsResponse {
|
||||
final String userL1;
|
||||
final String userL2;
|
||||
final bool enableIT;
|
||||
final bool enableIGC;
|
||||
final SpanData span;
|
||||
|
||||
const SpanDetailsResponse({
|
||||
required this.userL1,
|
||||
required this.userL2,
|
||||
required this.enableIGC,
|
||||
required this.enableIT,
|
||||
required this.span,
|
||||
});
|
||||
|
||||
factory SpanDetailsResponse.fromJson(Map<String, dynamic> json) =>
|
||||
SpanDetailsResponse(
|
||||
userL1: json['user_l1'] as String,
|
||||
userL2: json['user_l2'] as String,
|
||||
enableIT: json['enable_it'] as bool,
|
||||
enableIGC: json['enable_igc'] as bool,
|
||||
span: SpanData.fromJson(json['span']),
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/constants/match_rule_ids.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
||||
|
||||
class MatchStyleUtil {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/utils/match_style_util.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/widgets/igc/autocorrect_span.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';
|
||||
|
|
@ -31,7 +33,7 @@ class PangeaTextController extends TextEditingController {
|
|||
|
||||
bool forceKeepOpen = false;
|
||||
|
||||
setSystemText(String text, EditType type) {
|
||||
void setSystemText(String text, EditType type) {
|
||||
editType = type;
|
||||
this.text = text;
|
||||
}
|
||||
|
|
@ -66,46 +68,32 @@ class PangeaTextController extends TextEditingController {
|
|||
return;
|
||||
}
|
||||
|
||||
final int matchIndex =
|
||||
choreographer.igc.igcTextData!.getTopMatchIndexForOffset(
|
||||
final match = choreographer.igc.igcTextData!.getMatchByOffset(
|
||||
selection.baseOffset,
|
||||
);
|
||||
if (match == null) return;
|
||||
|
||||
// if autoplay on and it start then just start it
|
||||
if (matchIndex != -1 &&
|
||||
// choreographer.itAutoPlayEnabled &&
|
||||
choreographer.igc.igcTextData!.matches[matchIndex].isITStart) {
|
||||
return choreographer.onITStart(
|
||||
choreographer.igc.igcTextData!.matches[matchIndex],
|
||||
);
|
||||
if (match.updatedMatch.isITStart) {
|
||||
return choreographer.onITStart(match);
|
||||
}
|
||||
|
||||
final Widget? cardToShow = matchIndex != -1
|
||||
? SpanCard(
|
||||
matchIndex: matchIndex,
|
||||
choreographer: choreographer,
|
||||
)
|
||||
: null;
|
||||
|
||||
if (cardToShow != null) {
|
||||
MatrixState.pAnyState.closeAllOverlays(
|
||||
filter: RegExp(r'span_card_overlay_\d+'),
|
||||
);
|
||||
OverlayUtil.showPositionedCard(
|
||||
overlayKey: matchIndex != -1 ? "span_card_overlay_$matchIndex" : null,
|
||||
context: context,
|
||||
maxHeight: matchIndex != -1 &&
|
||||
choreographer.igc.igcTextData!.matches[matchIndex].isITStart
|
||||
? 260
|
||||
: 400,
|
||||
maxWidth: 350,
|
||||
cardToShow: cardToShow,
|
||||
transformTargetId: choreographer.inputTransformTargetKey,
|
||||
onDismiss: () => choreographer.setState(),
|
||||
ignorePointer: true,
|
||||
isScrollable: false,
|
||||
);
|
||||
}
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
OverlayUtil.showPositionedCard(
|
||||
overlayKey:
|
||||
"span_card_overlay_${match.updatedMatch.match.offset}_${match.updatedMatch.match.length}",
|
||||
context: context,
|
||||
maxHeight: 400,
|
||||
maxWidth: 350,
|
||||
cardToShow: SpanCard(
|
||||
match: match,
|
||||
choreographer: choreographer,
|
||||
),
|
||||
transformTargetId: choreographer.inputTransformTargetKey,
|
||||
onDismiss: () => choreographer.setState(),
|
||||
ignorePointer: true,
|
||||
isScrollable: false,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -145,22 +133,15 @@ class PangeaTextController extends TextEditingController {
|
|||
} else if (choreographer.igc.igcTextData == null || text.isEmpty) {
|
||||
return TextSpan(text: text, style: style);
|
||||
} else {
|
||||
final parts = text.split(choreographer.igc.igcTextData!.originalInput);
|
||||
final parts = text.split(choreographer.igc.igcTextData!.currentText);
|
||||
|
||||
if (parts.length == 1 || parts.length > 2) {
|
||||
return TextSpan(text: text, style: style);
|
||||
}
|
||||
|
||||
final choreoSteps = choreographer.choreoRecord?.choreoSteps;
|
||||
|
||||
List<InlineSpan> inlineSpans = [];
|
||||
try {
|
||||
inlineSpans = choreographer.igc.igcTextData!.constructTokenSpan(
|
||||
choreoSteps: (choreoSteps?.isNotEmpty ?? false) &&
|
||||
choreoSteps!.last.acceptedOrIgnoredMatch?.status ==
|
||||
PangeaMatchStatus.automatic
|
||||
? choreoSteps
|
||||
: [],
|
||||
inlineSpans = constructTokenSpan(
|
||||
defaultStyle: style,
|
||||
onUndo: choreographer.onUndoReplacement,
|
||||
);
|
||||
|
|
@ -181,4 +162,105 @@ class PangeaTextController extends TextEditingController {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
InlineSpan _matchSpan(
|
||||
PangeaMatchState match,
|
||||
TextStyle style,
|
||||
VoidCallback onUndo,
|
||||
) {
|
||||
if (match.updatedMatch.status == PangeaMatchStatus.automatic) {
|
||||
final span = choreographer.igc.igcTextData!.currentText.characters
|
||||
.getRange(
|
||||
match.updatedMatch.match.offset,
|
||||
match.updatedMatch.match.offset + match.updatedMatch.match.length,
|
||||
)
|
||||
.toString();
|
||||
|
||||
final originalText = match.originalMatch.match.fullText.characters
|
||||
.getRange(
|
||||
match.originalMatch.match.offset,
|
||||
match.originalMatch.match.offset + match.originalMatch.match.length,
|
||||
)
|
||||
.toString();
|
||||
|
||||
return AutocorrectSpan(
|
||||
transformTargetId:
|
||||
"autocorrection_${match.updatedMatch.match.offset}_${match.updatedMatch.match.length}",
|
||||
currentText: span,
|
||||
originalText: originalText,
|
||||
onUndo: onUndo,
|
||||
style: style,
|
||||
);
|
||||
} else {
|
||||
return TextSpan(
|
||||
text: choreographer.igc.igcTextData!.currentText.characters
|
||||
.getRange(
|
||||
match.updatedMatch.match.offset,
|
||||
match.updatedMatch.match.offset + match.updatedMatch.match.length,
|
||||
)
|
||||
.toString(),
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a list of [TextSpan]s used to display the text in the input field
|
||||
/// with the appropriate styling for each error match.
|
||||
List<InlineSpan> constructTokenSpan({
|
||||
required void Function(PangeaMatchState) onUndo,
|
||||
TextStyle? defaultStyle,
|
||||
}) {
|
||||
final automaticMatches = choreographer.igc.igcTextData!.closedMatches
|
||||
.where((m) => m.updatedMatch.status == PangeaMatchStatus.automatic)
|
||||
.toList();
|
||||
|
||||
final textSpanMatches = [
|
||||
...choreographer.igc.igcTextData!.openMatches,
|
||||
...automaticMatches,
|
||||
]..sort(
|
||||
(a, b) =>
|
||||
a.updatedMatch.match.offset.compareTo(b.updatedMatch.match.offset),
|
||||
);
|
||||
|
||||
final currentText = choreographer.igc.igcTextData!.currentText;
|
||||
|
||||
final spans = <InlineSpan>[];
|
||||
int cursor = 0;
|
||||
|
||||
for (final match in textSpanMatches) {
|
||||
if (cursor < match.updatedMatch.match.offset) {
|
||||
final text = currentText.characters
|
||||
.getRange(cursor, match.updatedMatch.match.offset)
|
||||
.toString();
|
||||
spans.add(TextSpan(text: text, style: defaultStyle));
|
||||
}
|
||||
|
||||
final openMatch =
|
||||
choreographer.igc.igcTextData?.openMatch?.updatedMatch.match;
|
||||
final style = MatchStyleUtil.textStyle(
|
||||
match.updatedMatch,
|
||||
defaultStyle,
|
||||
openMatch != null &&
|
||||
openMatch.offset == match.updatedMatch.match.offset &&
|
||||
openMatch.length == match.updatedMatch.match.length,
|
||||
);
|
||||
|
||||
spans.add(_matchSpan(match, style, () => onUndo.call(match)));
|
||||
cursor =
|
||||
match.updatedMatch.match.offset + match.updatedMatch.match.length;
|
||||
}
|
||||
|
||||
if (cursor < currentText.characters.length) {
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: currentText.characters
|
||||
.getRange(cursor, currentText.characters.length)
|
||||
.toString(),
|
||||
style: defaultStyle,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,8 @@ import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/span_choice_type.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/span_data_type.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
|
||||
import '../../../../widgets/matrix.dart';
|
||||
import '../../../bot/widgets/bot_face_svg.dart';
|
||||
|
|
@ -15,12 +14,12 @@ import '../choice_array.dart';
|
|||
import 'why_button.dart';
|
||||
|
||||
class SpanCard extends StatefulWidget {
|
||||
final int matchIndex;
|
||||
final PangeaMatchState match;
|
||||
final Choreographer choreographer;
|
||||
|
||||
const SpanCard({
|
||||
super.key,
|
||||
required this.matchIndex,
|
||||
required this.match,
|
||||
required this.choreographer,
|
||||
});
|
||||
|
||||
|
|
@ -30,19 +29,17 @@ class SpanCard extends StatefulWidget {
|
|||
|
||||
class SpanCardState extends State<SpanCard> {
|
||||
bool fetchingData = false;
|
||||
int? selectedChoiceIndex;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (pangeaMatch?.isITStart == true) {
|
||||
if (widget.match.updatedMatch.isITStart == true) {
|
||||
_onITStart();
|
||||
return;
|
||||
}
|
||||
|
||||
getSpanDetails();
|
||||
_fetchSelected();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -52,76 +49,19 @@ class SpanCardState extends State<SpanCard> {
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
PangeaMatch? get pangeaMatch {
|
||||
if (widget.choreographer.igc.igcTextData == null) return null;
|
||||
if (widget.matchIndex >=
|
||||
widget.choreographer.igc.igcTextData!.matches.length) {
|
||||
ErrorHandler.logError(
|
||||
m: "matchIndex out of bounds in span card",
|
||||
data: {
|
||||
"matchIndex": widget.matchIndex,
|
||||
"matchesLength": widget.choreographer.igc.igcTextData?.matches.length,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return widget.choreographer.igc.igcTextData?.matches[widget.matchIndex];
|
||||
}
|
||||
|
||||
//get selected choice
|
||||
SpanChoice? get selectedChoice {
|
||||
if (selectedChoiceIndex == null) return null;
|
||||
return _choiceByIndex(selectedChoiceIndex!);
|
||||
}
|
||||
|
||||
SpanChoice? _choiceByIndex(int index) {
|
||||
if (pangeaMatch?.match.choices == null ||
|
||||
pangeaMatch!.match.choices!.length <= index) {
|
||||
return null;
|
||||
}
|
||||
return pangeaMatch?.match.choices?[index];
|
||||
}
|
||||
|
||||
void _fetchSelected() {
|
||||
if (pangeaMatch?.match.choices == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if user ever selected the correct choice, automatically select it
|
||||
final selectedCorrectIndex =
|
||||
pangeaMatch!.match.choices!.indexWhere((choice) {
|
||||
return choice.selected && choice.isBestCorrection;
|
||||
});
|
||||
|
||||
if (selectedCorrectIndex != -1) {
|
||||
selectedChoiceIndex = selectedCorrectIndex;
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedChoiceIndex == null) {
|
||||
DateTime? mostRecent;
|
||||
final numChoices = pangeaMatch!.match.choices!.length;
|
||||
for (int i = 0; i < numChoices; i++) {
|
||||
final choice = _choiceByIndex(i);
|
||||
if (choice!.timestamp != null &&
|
||||
(mostRecent == null || choice.timestamp!.isAfter(mostRecent))) {
|
||||
mostRecent = choice.timestamp;
|
||||
selectedChoiceIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SpanChoice? get selectedChoice =>
|
||||
widget.match.updatedMatch.match.selectedChoice;
|
||||
|
||||
Future<void> getSpanDetails({bool force = false}) async {
|
||||
if (pangeaMatch?.isITStart ?? false) return;
|
||||
if (widget.match.updatedMatch.isITStart) return;
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
fetchingData = true;
|
||||
});
|
||||
|
||||
await widget.choreographer.igc.spanDataController.getSpanDetails(
|
||||
widget.matchIndex,
|
||||
await widget.choreographer.igc.setSpanDetails(
|
||||
match: widget.match,
|
||||
force: force,
|
||||
);
|
||||
|
||||
|
|
@ -131,28 +71,23 @@ class SpanCardState extends State<SpanCard> {
|
|||
}
|
||||
|
||||
void _onITStart() {
|
||||
if (widget.choreographer.itEnabled && pangeaMatch != null) {
|
||||
widget.choreographer.onITStart(pangeaMatch!);
|
||||
if (widget.choreographer.itEnabled) {
|
||||
widget.choreographer.onITStart(widget.match);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onChoiceSelect(int index) async {
|
||||
selectedChoiceIndex = index;
|
||||
if (selectedChoice != null) {
|
||||
selectedChoice!.timestamp = DateTime.now();
|
||||
selectedChoice!.selected = true;
|
||||
setState(
|
||||
() => (selectedChoice!.isBestCorrection
|
||||
? BotExpression.gold
|
||||
: BotExpression.surprised),
|
||||
);
|
||||
}
|
||||
void _onChoiceSelect(int index) {
|
||||
widget.match.selectChoice(index);
|
||||
setState(
|
||||
() => (selectedChoice!.isBestCorrection
|
||||
? BotExpression.gold
|
||||
: BotExpression.surprised),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onReplaceSelected() async {
|
||||
await widget.choreographer.onReplacementSelect(
|
||||
matchIndex: widget.matchIndex,
|
||||
choiceIndex: selectedChoiceIndex!,
|
||||
Future<void> _onAcceptReplacement() async {
|
||||
await widget.choreographer.onAcceptReplacement(
|
||||
match: widget.match,
|
||||
);
|
||||
_showFirstMatch();
|
||||
}
|
||||
|
|
@ -161,17 +96,14 @@ class SpanCardState extends State<SpanCard> {
|
|||
Future.delayed(
|
||||
Duration.zero,
|
||||
() {
|
||||
widget.choreographer.onIgnoreMatch(
|
||||
matchIndex: widget.matchIndex,
|
||||
);
|
||||
widget.choreographer.onIgnoreMatch(match: widget.match);
|
||||
_showFirstMatch();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showFirstMatch() {
|
||||
if (widget.choreographer.igc.igcTextData != null &&
|
||||
widget.choreographer.igc.igcTextData!.matches.isNotEmpty) {
|
||||
if (widget.choreographer.igc.canShowFirstMatch) {
|
||||
widget.choreographer.igc.showFirstMatch(context);
|
||||
} else {
|
||||
MatrixState.pAnyState.closeOverlay();
|
||||
|
|
@ -199,7 +131,7 @@ class WordMatchContent extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.pangeaMatch == null || controller.pangeaMatch!.isITStart) {
|
||||
if (controller.widget.match.updatedMatch.isITStart) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
|
|
@ -218,21 +150,24 @@ class WordMatchContent extends StatelessWidget {
|
|||
children: [
|
||||
const SizedBox(height: 8),
|
||||
ChoicesArray(
|
||||
originalSpan: controller.pangeaMatch!.matchContent,
|
||||
originalSpan:
|
||||
controller.widget.match.updatedMatch.matchContent,
|
||||
isLoading: controller.fetchingData,
|
||||
choices: controller.pangeaMatch!.match.choices
|
||||
?.map(
|
||||
(e) => Choice(
|
||||
text: e.value,
|
||||
color: e.selected ? e.type.color : null,
|
||||
isGold: e.type.name == 'bestCorrection',
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
choices:
|
||||
controller.widget.match.updatedMatch.match.choices
|
||||
?.map(
|
||||
(e) => Choice(
|
||||
text: e.value,
|
||||
color: e.selected ? e.type.color : null,
|
||||
isGold: e.type.name == 'bestCorrection',
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onPressed: (value, index) =>
|
||||
controller._onChoiceSelect(index),
|
||||
selectedChoiceIndex: controller.selectedChoiceIndex,
|
||||
id: controller.pangeaMatch!.hashCode.toString(),
|
||||
selectedChoiceIndex: controller
|
||||
.widget.match.updatedMatch.match.selectedChoiceIndex,
|
||||
id: controller.widget.match.hashCode.toString(),
|
||||
langCode: MatrixState.pangeaController.languageController
|
||||
.activeL2Code(),
|
||||
),
|
||||
|
|
@ -269,10 +204,10 @@ class WordMatchContent extends StatelessWidget {
|
|||
),
|
||||
Expanded(
|
||||
child: Opacity(
|
||||
opacity: controller.selectedChoiceIndex != null ? 1.0 : 0.5,
|
||||
opacity: controller.selectedChoice != null ? 1.0 : 0.5,
|
||||
child: TextButton(
|
||||
onPressed: controller.selectedChoiceIndex != null
|
||||
? controller._onReplaceSelected
|
||||
onPressed: controller.selectedChoice != null
|
||||
? controller._onAcceptReplacement
|
||||
: null,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all<Color>(
|
||||
|
|
@ -315,12 +250,8 @@ class PromptAndFeedback extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (controller.pangeaMatch == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: controller.pangeaMatch!.isITStart
|
||||
constraints: controller.widget.match.updatedMatch.isITStart
|
||||
? null
|
||||
: const BoxConstraints(minHeight: 75.0),
|
||||
child: Column(
|
||||
|
|
@ -352,10 +283,9 @@ class PromptAndFeedback extends StatelessWidget {
|
|||
loading: controller.fetchingData,
|
||||
),
|
||||
],
|
||||
if (!controller.fetchingData &&
|
||||
controller.selectedChoiceIndex == null)
|
||||
if (!controller.fetchingData && controller.selectedChoice == null)
|
||||
Text(
|
||||
controller.pangeaMatch!.match.type.typeName
|
||||
controller.widget.match.updatedMatch.match.type.typeName
|
||||
.defaultPrompt(context),
|
||||
style: BotStyle.text(context).copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
|
|
|
|||
|
|
@ -343,7 +343,9 @@ class ITChoices extends StatelessWidget {
|
|||
continuance.feedbackText(context),
|
||||
);
|
||||
}
|
||||
controller.currentITStep!.continuances[index].wasClicked = true;
|
||||
controller.currentITStep!.continuances[index] = continuance.copyWith(
|
||||
wasClicked: true,
|
||||
);
|
||||
controller.choreographer.setState();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,8 +62,7 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
}
|
||||
|
||||
void _showFirstMatch() {
|
||||
final igcData = widget.controller.choreographer.igc.igcTextData;
|
||||
if (igcData != null && igcData.matches.isNotEmpty) {
|
||||
if (widget.controller.choreographer.igc.canShowFirstMatch) {
|
||||
widget.controller.choreographer.igc.showFirstMatch(context);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
|
||||
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
|
||||
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
|
||||
import 'package:fluffychat/pangea/toolbar/models/speech_to_text_models.dart';
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class PrefKey {
|
|||
static const languagesKey = 'p_lang_flag';
|
||||
}
|
||||
|
||||
final LanguageDetection unknownLanguageDetection = LanguageDetection(
|
||||
const LanguageDetection unknownLanguageDetection = LanguageDetection(
|
||||
langCode: LanguageKeys.unknownLanguage,
|
||||
confidence: 0.5,
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue