You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fluffychat/lib/pangea/choreographer/controllers/it_controller.dart

443 lines
13 KiB
Dart

import 'dart:async';
import 'dart:developer';
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:sentry_flutter/sentry_flutter.dart';
import '../../models/custom_input_translation_model.dart';
import '../../models/it_response_model.dart';
import '../../models/it_step.dart';
import '../../models/system_choice_translation_model.dart';
import '../../repo/interactive_translation_repo.dart';
import '../../repo/message_service.repo.dart';
import 'choreographer.dart';
class ITController {
Choreographer choreographer;
bool _isOpen = false;
bool _isEditingSourceText = false;
bool showChoiceFeedback = false;
ITStartData? _itStartData;
String? sourceText;
List<ITStep> completedITSteps = [];
CurrentITStep? currentITStep;
GoldRouteTracker goldRouteTracker = GoldRouteTracker.defaultTracker;
List<int> payLoadIds = [];
ITController(this.choreographer);
void clear() {
_isOpen = false;
showChoiceFeedback = false;
_isEditingSourceText = false;
_itStartData = null;
sourceText = null;
completedITSteps = [];
currentITStep = null;
goldRouteTracker = GoldRouteTracker.defaultTracker;
payLoadIds = [];
choreographer.altTranslator.clear();
choreographer.errorService.resetError();
choreographer.choreoMode = ChoreoMode.igc;
choreographer.setState();
}
Future<void> initializeIT(ITStartData itStartData) async {
Future.delayed(const Duration(microseconds: 100), () {
_isOpen = true;
});
_itStartData = itStartData;
}
void closeIT() {
//if they close it before choosing anything, just put their text back
//PTODO - explore using last itStep
if (choreographer.currentText.isEmpty) {
choreographer.textController.text = sourceText ?? "";
}
clear();
}
/// if IGC isn't positive that text is full L1 then translate to L1
Future<void> _setSourceText() async {
// try {
if (_itStartData == null || _itStartData!.text.isEmpty) {
Sentry.addBreadcrumb(
Breadcrumb(
message: "choreo context",
data: {
"igcTextData": choreographer.igc.igcTextData?.toJson(),
"currentText": choreographer.currentText,
},
),
);
throw Exception("null _itStartData or empty text in _setSourceText");
}
debugPrint("_setSourceText with detectedLang ${_itStartData!.langCode}");
if (_itStartData!.langCode == choreographer.l1LangCode) {
sourceText = _itStartData!.text;
return;
}
final FullTextTranslationResponseModel res =
await FullTextTranslationRepo.translate(
accessToken: await choreographer.accessToken,
request: FullTextTranslationRequestModel(
text: _itStartData!.text,
tgtLang: choreographer.l1LangCode!,
srcLang: choreographer.l2LangCode,
userL1: choreographer.l1LangCode!,
userL2: choreographer.l2LangCode!,
),
);
sourceText = res.bestTranslation;
// } catch (err, stack) {
// debugger(when: kDebugMode);
// if (_itStartData?.text.isNotEmpty ?? false) {
// ErrorHandler.logError(e: err, s: stack);
// sourceText = _itStartData!.text;
// } else {
// rethrow;
// }
// }
}
// used 1) at very beginning (with custom input = null)
// and 2) if they make direct edits to the text field
Future<void> getTranslationData(bool useCustomInput) async {
try {
choreographer.startLoading();
final String currentText = choreographer.currentText;
if (sourceText == null) await _setSourceText();
if (useCustomInput && currentITStep != null) {
completedITSteps.add(
ITStep(
currentITStep!.continuances,
customInput: currentText,
),
);
}
currentITStep = null;
final ITResponseModel res = await _customInputTranslation(currentText);
// final ITResponseModel res = await (useCustomInput ||
// currentText.isEmpty ||
// translationId == null ||
// completedITSteps.last.chosenContinuance?.indexSavedByServer ==
// null
// ? _customInputTranslation(currentText)
// : _systemChoiceTranslation(translationId));
if (res.goldContinuances != null && res.goldContinuances!.isNotEmpty) {
goldRouteTracker = GoldRouteTracker(
res.goldContinuances!,
sourceText!,
);
}
currentITStep = CurrentITStep(
sourceText: sourceText!,
currentText: currentText,
responseModel: res,
storedGoldContinuances: goldRouteTracker.continuances,
);
_addPayloadId(res);
if (isTranslationDone) {
choreographer.altTranslator.setTranslationFeedback();
choreographer.getLanguageHelp(true);
}
} catch (e, s) {
debugger(when: kDebugMode);
if (e is! http.Response) {
ErrorHandler.logError(e: e, s: s);
}
choreographer.errorService.setErrorAndLock(
ChoreoError(type: ChoreoErrorType.unknown, raw: e),
);
} finally {
choreographer.stopLoading();
}
}
Future<void> onEditSourceTextSubmit(String newSourceText) async {
try {
sourceText = newSourceText;
_isEditingSourceText = false;
final String currentText = choreographer.currentText;
choreographer.startLoading();
final List<ITResponseModel> responses = await Future.wait([
_customInputTranslation(""),
_customInputTranslation(choreographer.currentText),
]);
if (responses[0].goldContinuances != null &&
responses[0].goldContinuances!.isNotEmpty) {
goldRouteTracker = GoldRouteTracker(
responses[0].goldContinuances!,
sourceText!,
);
}
currentITStep = CurrentITStep(
sourceText: sourceText!,
currentText: currentText,
responseModel: responses[1],
storedGoldContinuances: goldRouteTracker.continuances,
);
_addPayloadId(responses[1]);
} catch (err, stack) {
debugger(when: kDebugMode);
if (err is! http.Response) {
ErrorHandler.logError(e: err, s: stack);
}
choreographer.errorService.setErrorAndLock(
ChoreoError(type: ChoreoErrorType.unknown, raw: err),
);
} finally {
choreographer.stopLoading();
}
}
Future<ITResponseModel> _customInputTranslation(String textInput) async {
return ITRepo.customInputTranslate(
CustomInputRequestModel(
//this should be set by this time
text: sourceText!,
customInput: textInput,
sourceLangCode: sourceLangCode,
targetLangCode: targetLangCode,
userId: choreographer.userId!,
roomId: choreographer.roomId!,
classId: choreographer.classId,
goldTranslation: goldRouteTracker.fullTranslation,
goldContinuances: goldRouteTracker.continuances,
),
);
}
// used when user selects a choice
Future<ITResponseModel> _systemChoiceTranslation(String translationId) =>
ITRepo.systemChoiceTranslate(
SystemChoiceRequestModel(
userId: choreographer.userId!,
nextWordIndex:
completedITSteps.last.chosenContinuance?.indexSavedByServer,
roomId: choreographer.roomId!,
translationId: translationId,
targetLangCode: targetLangCode,
sourceLangCode: sourceLangCode,
classId: choreographer.classId,
),
);
MessageServiceModel? messageServiceModelWithMessageId() =>
usedInteractiveTranslation
? MessageServiceModel(
classId: choreographer.classId,
roomId: choreographer.roomId,
message: choreographer.currentText,
messageId: null,
payloadIds: payLoadIds,
userId: choreographer.userId!,
l1Lang: sourceLangCode,
l2Lang: targetLangCode,
)
: null;
//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);
completedITSteps.add(itStep);
showChoiceFeedback = true;
Future.delayed(
const Duration(
milliseconds: ChoreoConstants.millisecondsToDisplayFeedback,
),
() {
showChoiceFeedback = false;
choreographer.setState();
},
);
choreographer.onITChoiceSelect(itStep);
choreographer.setState();
}
String get uniqueKeyForLayerLink => "itChoices${choreographer.roomId}";
void _addPayloadId(ITResponseModel res) {
payLoadIds.add(res.payloadId);
}
bool get usedInteractiveTranslation => sourceText != null;
bool get isTranslationDone => currentITStep != null && currentITStep!.isFinal;
bool get isOpen => _isOpen;
String get targetLangCode => choreographer.l2LangCode!;
String get sourceLangCode => choreographer.l1LangCode!;
bool get isLoading => choreographer.isFetching;
bool get correctChoicesSelected =>
completedITSteps.every((ITStep step) => step.isCorrect);
String latestChoiceFeedback(BuildContext context) =>
completedITSteps.isNotEmpty
? completedITSteps.last.choiceFeedback(context)
: "";
// String translationFeedback(BuildContext context) =>
// completedITSteps.isNotEmpty
// ? completedITSteps.last.translationFeedback(context)
// : "";
bool get showAlternativeTranslationsOption => completedITSteps.isNotEmpty
? completedITSteps.last.showAlternativeTranslationOption &&
sourceText != null
: false;
setIsEditingSourceText(bool value) {
_isEditingSourceText = value;
choreographer.setState();
}
bool get isEditingSourceText => _isEditingSourceText;
int get correctChoices =>
completedITSteps.where((element) => element.isCorrect).length;
int get wildcardChoices =>
completedITSteps.where((element) => element.isYellow).length;
int get incorrectChoices =>
completedITSteps.where((element) => element.isWrong).length;
int get customChoices =>
completedITSteps.where((element) => element.isCustom).length;
bool get allCorrect => completedITSteps.every((element) => element.isCorrect);
}
class ITStartData {
String text;
String? langCode;
ITStartData(this.text, this.langCode);
}
class GoldRouteTracker {
late String _originalText;
List<Continuance> continuances;
GoldRouteTracker(this.continuances, String originalText) {
_originalText = originalText;
}
static get defaultTracker => GoldRouteTracker([], "");
Continuance? currentContinuance({
required String currentText,
required String sourceText,
}) {
if (_originalText != sourceText) {
debugPrint("$_originalText != $_originalText");
return null;
}
String stack = "";
for (final cont in continuances) {
if (stack == currentText) {
return cont;
}
stack += cont.text;
}
return null;
}
String? get fullTranslation {
if (continuances.isEmpty) return null;
String full = "";
for (final cont in continuances) {
full += cont.text;
}
return full;
}
}
class CurrentITStep {
late List<Continuance> continuances;
late bool isFinal;
late String? translationId;
late int payloadId;
CurrentITStep({
required String sourceText,
required String currentText,
required ITResponseModel responseModel,
required List<Continuance>? storedGoldContinuances,
}) {
final List<Continuance> gold =
storedGoldContinuances ?? responseModel.goldContinuances ?? [];
final goldTracker = GoldRouteTracker(gold, sourceText);
isFinal = responseModel.isFinal;
translationId = responseModel.translationId;
payloadId = responseModel.payloadId;
if (responseModel.continuances.isEmpty) {
continuances = [];
} else {
final Continuance? goldCont = goldTracker.currentContinuance(
currentText: currentText,
sourceText: sourceText,
);
if (goldCont != null) {
continuances = [
...responseModel.continuances
.where((c) => c.text.toLowerCase() != goldCont.text.toLowerCase())
.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;
}),
goldCont,
];
continuances.shuffle();
} else {
continuances = responseModel.continuances;
}
}
}
// get continuance with highest level
Continuance get best =>
continuances.reduce((a, b) => a.level > b.level ? a : b);
}