move logic for continuation of IT fully into IT controller, fix some issues with IT step request queue

This commit is contained in:
ggurdin 2025-11-04 10:42:35 -05:00
parent 978d70822f
commit ef8292b46c
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
9 changed files with 352 additions and 346 deletions

View file

@ -15,7 +15,7 @@ import 'package:fluffychat/pangea/choreographer/enums/choreo_mode.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/completed_it_step.dart';
import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.dart';
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
@ -91,7 +91,7 @@ class Choreographer extends ChangeNotifier {
}
void clear() {
_choreoMode = ChoreoMode.igc;
setChoreoMode(ChoreoMode.igc);
_lastChecked = null;
_timesClicked = 0;
_isFetching.value = false;
@ -105,6 +105,7 @@ class Choreographer extends ChangeNotifier {
@override
void dispose() {
super.dispose();
itController.dispose();
errorService.dispose();
textController.dispose();
_languageStream?.cancel();
@ -234,6 +235,10 @@ class Choreographer extends ChangeNotifier {
_lastChecked = textController.text;
if (textController.editType == EditType.it) {
return;
}
if (textController.editType == EditType.igc ||
textController.editType == EditType.itDismissed) {
textController.editType = EditType.keyboard;
@ -247,15 +252,10 @@ class Choreographer extends ChangeNotifier {
igc.clear();
_resetDebounceTimer();
if (textController.editType == EditType.it) {
_getLanguageAssistance();
} else {
_sourceText.value = null;
_debounceTimer ??= Timer(
const Duration(milliseconds: ChoreoConstants.msBeforeIGCStart),
() => _getLanguageAssistance(),
);
}
_debounceTimer ??= Timer(
const Duration(milliseconds: ChoreoConstants.msBeforeIGCStart),
() => _getLanguageAssistance(),
);
//Note: we don't set the keyboard type on each keyboard stroke so this is how we default to
//a change being from the keyboard unless explicitly set to one of the other
@ -286,7 +286,7 @@ class Choreographer extends ChangeNotifier {
_initChoreoRecord();
_startLoading();
await (isRunningIT ? itController.continueIT() : igc.getIGCTextData());
await igc.getIGCTextData();
_stopLoading();
}
@ -398,7 +398,7 @@ class Choreographer extends ChangeNotifier {
}
void onAcceptContinuance(int index) {
final step = itController.getAcceptedITStep(index);
final step = itController.onAcceptContinuance(index);
textController.setSystemText(
textController.text + step.continuances[step.chosen].text,
EditType.it,

View file

@ -1,26 +1,28 @@
import 'dart:async';
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:async/async.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/choreo_mode.dart';
import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
import 'package:fluffychat/pangea/choreographer/models/gold_route_tracker.dart';
import 'package:fluffychat/pangea/choreographer/models/it_step.dart';
import 'package:fluffychat/pangea/choreographer/repo/it_repo.dart';
import 'package:fluffychat/pangea/choreographer/repo/it_response_model.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../models/it_step.dart';
import '../models/completed_it_step.dart';
import '../repo/it_request_model.dart';
import '../repo/it_response_model.dart';
import 'choreographer.dart';
class ITController {
final Choreographer _choreographer;
ValueNotifier<ITStep?> _currentITStep = ValueNotifier(null);
final List<Completer<ITStep>> _queue = [];
final ValueNotifier<ITStep?> _currentITStep = ValueNotifier(null);
final Queue<Completer<ITStep>> _queue = Queue();
GoldRouteTracker? _goldRouteTracker;
final ValueNotifier<bool> _open = ValueNotifier(false);
@ -52,8 +54,37 @@ class ITController {
);
}
Future<Result<ITResponseModel>> _safeRequest(String text) {
return ITRepo.get(_request(text)).timeout(
const Duration(seconds: 10),
onTimeout: () => Result.error(
TimeoutException("ITRepo.get timed out after 10 seconds"),
),
);
}
void clear({bool dismissed = false}) {
MatrixState.pAnyState.closeOverlay("it_feedback_card");
_open.value = false;
_editing.value = false;
_dismissed = dismissed;
_queue.clear();
_currentITStep.value = null;
_goldRouteTracker = null;
_choreographer.setChoreoMode(ChoreoMode.igc);
_choreographer.setSourceText(null);
}
void dispose() {
_currentITStep.dispose();
_editing.dispose();
}
void openIT() {
_open.value = true;
continueIT();
}
void closeIT() {
@ -68,20 +99,6 @@ class ITController {
clear(dismissed: true);
}
void clear({bool dismissed = false}) {
MatrixState.pAnyState.closeOverlay("it_feedback_card");
_open.value = false;
_editing.value = false;
_dismissed = dismissed;
_queue.clear();
_currentITStep = ValueNotifier(null);
_goldRouteTracker = null;
_choreographer.setChoreoMode(ChoreoMode.igc);
_choreographer.setSourceText(null);
}
void setEditing(bool value) {
_editing.value = value;
}
@ -89,7 +106,7 @@ class ITController {
void onSubmitEdits() {
_editing.value = false;
_queue.clear();
_currentITStep = ValueNotifier(null);
_currentITStep.value = null;
_goldRouteTracker = null;
continueIT();
}
@ -113,54 +130,49 @@ class ITController {
return _currentITStep.value!.continuances[index];
}
CompletedITStep getAcceptedITStep(int chosenIndex) {
CompletedITStep onAcceptContinuance(int chosenIndex) {
if (_currentITStep.value == null) {
throw "getAcceptedITStep called when _currentITStep is null";
throw "onAcceptContinuance called when _currentITStep is null";
}
if (chosenIndex < 0 ||
chosenIndex >= _currentITStep.value!.continuances.length) {
throw "getAcceptedITStep called with invalid index $chosenIndex";
throw "onAcceptContinuance called with invalid index $chosenIndex";
}
return CompletedITStep(
final completedStep = CompletedITStep(
_currentITStep.value!.continuances,
chosen: chosenIndex,
);
continueIT();
return completedStep;
}
bool _continuing = false;
Future<void> continueIT() async {
if (_currentITStep.value == null) {
await _initTranslationData();
return;
}
if (_queue.isEmpty) {
_choreographer.closeIT();
} else {
try {
final nextStepCompleter = _queue.removeAt(0);
if (_continuing) return;
_continuing = true;
try {
if (_currentITStep.value == null) {
await _initTranslationData();
} else if (_queue.isEmpty) {
_choreographer.closeIT();
} else {
final nextStepCompleter = _queue.removeFirst();
_currentITStep.value = await nextStepCompleter.future;
} catch (e) {
if (_open.value) {
_choreographer.errorService.setErrorAndLock(
ChoreoError(raw: e),
);
}
}
} catch (e) {
_choreographer.errorService.setErrorAndLock(ChoreoError(raw: e));
} finally {
_continuing = false;
}
}
Future<void> _initTranslationData() async {
final String currentText = _choreographer.currentText;
final res = await ITRepo.get(_request(currentText)).timeout(
const Duration(seconds: 10),
onTimeout: () {
return Result.error(
TimeoutException("ITRepo.get timed out after 10 seconds"),
);
},
);
final res = await _safeRequest(currentText);
if (_sourceText.value == null || !_open.value) return;
if (res.isError || res.result?.goldContinuances == null) {
_choreographer.errorService.setErrorAndLock(
@ -193,135 +205,25 @@ class ITController {
final sourceText = _sourceText.value!;
final goldContinuances = _goldRouteTracker!.continuances;
String currentText =
_choreographer.currentText + _goldRouteTracker!.continuances[0].text;
for (int i = 1; i < _goldRouteTracker!.continuances.length; i++) {
_queue.add(Completer<ITStep>());
final res = await ITRepo.get(_request(currentText)).timeout(
const Duration(seconds: 10),
onTimeout: () {
return Result.error(
TimeoutException("ITRepo.get timed out after 10 seconds"),
);
},
);
if (_queue.isEmpty) break;
if (res.isError) {
_queue.last.completeError(res.asError!);
String currentText = goldContinuances[0].text;
for (int i = 1; i < goldContinuances.length; i++) {
final completer = Completer<ITStep>();
_queue.add(completer);
final resp = await _safeRequest(currentText);
if (resp.isError) {
completer.completeError(resp.asError!);
break;
} else {
final step = ITStep.fromResponse(
sourceText: sourceText,
currentText: currentText,
responseModel: res.result!,
responseModel: resp.result!,
storedGoldContinuances: goldContinuances,
);
_queue.last.complete(step);
completer.complete(step);
}
currentText += goldContinuances[i].text;
}
}
}
class GoldRouteTracker {
final String _originalText;
final List<Continuance> continuances;
const GoldRouteTracker(this.continuances, String originalText)
: _originalText = originalText;
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 ITStep {
late List<Continuance> continuances;
late bool isFinal;
ITStep({this.continuances = const [], this.isFinal = false});
factory ITStep.fromResponse({
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);
final isFinal = responseModel.isFinal;
List<Continuance> continuances;
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) {
return e.copyWith(
level: ChoreoConstants.levelThresholdForYellow,
);
}
return e;
}),
goldCont,
];
continuances.shuffle();
} else {
continuances = List<Continuance>.from(responseModel.continuances);
}
}
return ITStep(
continuances: continuances,
isFinal: isFinal,
);
}
ITStep copyWith({
List<Continuance>? continuances,
bool? isFinal,
}) {
return ITStep(
continuances: continuances ?? this.continuances,
isFinal: isFinal ?? this.isFinal,
);
}
}

View file

@ -4,7 +4,7 @@ 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';
import 'it_step.dart';
import 'completed_it_step.dart';
/// this class lives within a [PangeaIGCEvent]
/// it always has a [RepresentationEvent] parent

View file

@ -0,0 +1,171 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import '../constants/choreo_constants.dart';
class CompletedITStep {
final List<Continuance> continuances;
final int chosen;
const CompletedITStep(
this.continuances, {
required this.chosen,
});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['continuances'] = continuances.map((e) => e.toJson(true)).toList();
data['chosen'] = chosen;
return data;
}
factory CompletedITStep.fromJson(Map<String, dynamic> json) {
final List<Continuance> continuances = <Continuance>[];
for (final Map<String, dynamic> continuance in json['continuances']) {
continuances.add(Continuance.fromJson(continuance));
}
return CompletedITStep(
continuances,
chosen: json['chosen'],
);
}
Continuance? get chosenContinuance {
return continuances[chosen];
}
}
class Continuance {
final double probability;
final int level;
final String text;
final String description;
final int? indexSavedByServer;
final bool wasClicked;
final bool inDictionary;
final bool hasInfo;
final bool gold;
const Continuance({
required this.probability,
required this.level,
required this.text,
required this.description,
required this.indexSavedByServer,
required this.wasClicked,
required this.inDictionary,
required this.hasInfo,
required this.gold,
});
factory Continuance.fromJson(Map<String, dynamic> json) {
return Continuance(
probability: json['probability'].toDouble(),
level: json['level'],
text: json['text'],
description: json['description'] ?? "",
indexSavedByServer: json["index"],
inDictionary: json['in_dictionary'] ?? true,
wasClicked: json['clkd'] ?? false,
hasInfo: json['has_info'] ?? false,
gold: json['gold'] ?? false,
);
}
Map<String, dynamic> toJson([bool condensed = false]) {
final Map<String, dynamic> data = <String, dynamic>{};
data['probability'] = probability;
data['level'] = level;
data['text'] = text;
data['clkd'] = wasClicked;
if (!condensed) {
data['description'] = description;
data['in_dictionary'] = inDictionary;
data['has_info'] = hasInfo;
data["index"] = indexSavedByServer;
data['gold'] = gold;
}
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) {
case ChoreoConstants.levelThresholdForGreen:
return ChoreoConstants.green;
case ChoreoConstants.levelThresholdForYellow:
return ChoreoConstants.yellow;
case ChoreoConstants.levelThresholdForRed:
return ChoreoConstants.red;
default:
return null;
}
}
String? feedbackText(BuildContext context) {
final L10n l10n = L10n.of(context);
switch (level) {
case ChoreoConstants.levelThresholdForGreen:
return l10n.greenFeedback;
case ChoreoConstants.levelThresholdForYellow:
return l10n.yellowFeedback;
case ChoreoConstants.levelThresholdForRed:
return l10n.redFeedback;
default:
return null;
}
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Continuance &&
runtimeType == other.runtimeType &&
probability == other.probability &&
level == other.level &&
text == other.text &&
description == other.description &&
indexSavedByServer == other.indexSavedByServer &&
wasClicked == other.wasClicked &&
inDictionary == other.inDictionary &&
hasInfo == other.hasInfo &&
gold == other.gold;
@override
int get hashCode =>
probability.hashCode ^
level.hashCode ^
text.hashCode ^
description.hashCode ^
indexSavedByServer.hashCode ^
wasClicked.hashCode ^
inDictionary.hashCode ^
hasInfo.hashCode ^
gold.hashCode;
}

View file

@ -0,0 +1,37 @@
import 'package:fluffychat/pangea/choreographer/models/completed_it_step.dart';
class GoldRouteTracker {
final String _originalText;
final List<Continuance> continuances;
const GoldRouteTracker(this.continuances, String originalText)
: _originalText = originalText;
Continuance? currentContinuance({
required String currentText,
required String sourceText,
}) {
if (_originalText != sourceText) {
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;
}
}

View file

@ -1,171 +1,67 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/models/completed_it_step.dart';
import 'package:fluffychat/pangea/choreographer/models/gold_route_tracker.dart';
import 'package:fluffychat/pangea/choreographer/repo/it_response_model.dart';
import 'package:fluffychat/l10n/l10n.dart';
import '../constants/choreo_constants.dart';
class ITStep {
late List<Continuance> continuances;
late bool isFinal;
class CompletedITStep {
final List<Continuance> continuances;
final int chosen;
ITStep({this.continuances = const [], this.isFinal = false});
const CompletedITStep(
this.continuances, {
required this.chosen,
});
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['continuances'] = continuances.map((e) => e.toJson(true)).toList();
data['chosen'] = chosen;
return data;
}
factory CompletedITStep.fromJson(Map<String, dynamic> json) {
final List<Continuance> continuances = <Continuance>[];
for (final Map<String, dynamic> continuance in json['continuances']) {
continuances.add(Continuance.fromJson(continuance));
}
return CompletedITStep(
continuances,
chosen: json['chosen'],
);
}
Continuance? get chosenContinuance {
return continuances[chosen];
}
}
class Continuance {
final double probability;
final int level;
final String text;
final String description;
final int? indexSavedByServer;
final bool wasClicked;
final bool inDictionary;
final bool hasInfo;
final bool gold;
const Continuance({
required this.probability,
required this.level,
required this.text,
required this.description,
required this.indexSavedByServer,
required this.wasClicked,
required this.inDictionary,
required this.hasInfo,
required this.gold,
});
factory Continuance.fromJson(Map<String, dynamic> json) {
return Continuance(
probability: json['probability'].toDouble(),
level: json['level'],
text: json['text'],
description: json['description'] ?? "",
indexSavedByServer: json["index"],
inDictionary: json['in_dictionary'] ?? true,
wasClicked: json['clkd'] ?? false,
hasInfo: json['has_info'] ?? false,
gold: json['gold'] ?? false,
);
}
Map<String, dynamic> toJson([bool condensed = false]) {
final Map<String, dynamic> data = <String, dynamic>{};
data['probability'] = probability;
data['level'] = level;
data['text'] = text;
data['clkd'] = wasClicked;
if (!condensed) {
data['description'] = description;
data['in_dictionary'] = inDictionary;
data['has_info'] = hasInfo;
data["index"] = indexSavedByServer;
data['gold'] = gold;
}
return data;
}
Continuance copyWith({
double? probability,
int? level,
String? text,
String? description,
int? indexSavedByServer,
bool? wasClicked,
bool? inDictionary,
bool? hasInfo,
bool? gold,
factory ITStep.fromResponse({
required String sourceText,
required String currentText,
required ITResponseModel responseModel,
required List<Continuance>? storedGoldContinuances,
}) {
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,
final List<Continuance> gold =
storedGoldContinuances ?? responseModel.goldContinuances ?? [];
final goldTracker = GoldRouteTracker(gold, sourceText);
final isFinal = responseModel.isFinal;
List<Continuance> continuances;
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) {
return e.copyWith(
level: ChoreoConstants.levelThresholdForYellow,
);
}
return e;
}),
goldCont,
];
continuances.shuffle();
} else {
continuances = List<Continuance>.from(responseModel.continuances);
}
}
return ITStep(
continuances: continuances,
isFinal: isFinal,
);
}
Color? get color {
if (!wasClicked) return null;
switch (level) {
case ChoreoConstants.levelThresholdForGreen:
return ChoreoConstants.green;
case ChoreoConstants.levelThresholdForYellow:
return ChoreoConstants.yellow;
case ChoreoConstants.levelThresholdForRed:
return ChoreoConstants.red;
default:
return null;
}
ITStep copyWith({
List<Continuance>? continuances,
bool? isFinal,
}) {
return ITStep(
continuances: continuances ?? this.continuances,
isFinal: isFinal ?? this.isFinal,
);
}
String? feedbackText(BuildContext context) {
final L10n l10n = L10n.of(context);
switch (level) {
case ChoreoConstants.levelThresholdForGreen:
return l10n.greenFeedback;
case ChoreoConstants.levelThresholdForYellow:
return l10n.yellowFeedback;
case ChoreoConstants.levelThresholdForRed:
return l10n.redFeedback;
default:
return null;
}
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Continuance &&
runtimeType == other.runtimeType &&
probability == other.probability &&
level == other.level &&
text == other.text &&
description == other.description &&
indexSavedByServer == other.indexSavedByServer &&
wasClicked == other.wasClicked &&
inDictionary == other.inDictionary &&
hasInfo == other.hasInfo &&
gold == other.gold;
@override
int get hashCode =>
probability.hashCode ^
level.hashCode ^
text.hashCode ^
description.hashCode ^
indexSavedByServer.hashCode ^
wasClicked.hashCode ^
inDictionary.hashCode ^
hasInfo.hashCode ^
gold.hashCode;
}

View file

@ -1,6 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:fluffychat/pangea/choreographer/models/it_step.dart';
import 'package:fluffychat/pangea/choreographer/models/completed_it_step.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
class ITRequestModel {

View file

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/models/it_step.dart';
import 'package:fluffychat/pangea/choreographer/models/completed_it_step.dart';
class ITResponseModel {
final String fullTextTranslation;

View file

@ -9,7 +9,7 @@ import 'package:fluffychat/pangea/choreographer/constants/choreo_constants.dart'
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choregrapher_user_settings_extension.dart';
import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreographer_ui_extension.dart';
import 'package:fluffychat/pangea/choreographer/models/it_step.dart';
import 'package:fluffychat/pangea/choreographer/models/completed_it_step.dart';
import 'package:fluffychat/pangea/choreographer/repo/full_text_translation_request_model.dart';
import 'package:fluffychat/pangea/choreographer/widgets/igc/word_data_card.dart';
import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart';