5825 remove unreferenced writing assistance code (#5826)

* chore: delete span details

* remove IT

* fix null check error

* more cleanup
This commit is contained in:
ggurdin 2026-02-26 14:09:45 -05:00 committed by GitHub
parent 43628427c1
commit 774432ef49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 68 additions and 1842 deletions

View file

@ -572,7 +572,6 @@ class ChatController extends State<ChatPageWithRoom>
void _pangeaInit() {
choreographer = Choreographer(inputFocus);
choreographer.timesDismissedIT.addListener(_onCloseIT);
final updater = Matrix.of(context).analyticsDataService.updateDispatcher;
_levelSubscription = updater.levelUpdateStream.stream.listen(_onLevelUp);
@ -861,7 +860,6 @@ class ChatController extends State<ChatPageWithRoom>
_constructsSubscription?.cancel();
_tokensSubscription?.cancel();
_router.routeInformationProvider.removeListener(_onRouteChanged);
choreographer.timesDismissedIT.removeListener(_onCloseIT);
scrollController.dispose();
inputFocus.dispose();
depressMessageButton.dispose();
@ -2287,11 +2285,6 @@ class ChatController extends State<ChatPageWithRoom>
return;
}
if (matchToShow.updatedMatch.isITStart) {
choreographer.itController.openIT(sendController.text);
return;
}
final isSpanCardOpen = MatrixState.pAnyState.isOverlayOpen(
overlayKey: 'span-card-overlay',
);
@ -2397,12 +2390,6 @@ class ChatController extends State<ChatPageWithRoom>
}
}
void _onCloseIT() {
if (choreographer.timesDismissedIT.value >= 3) {
showDisableLanguageToolsPopup();
}
}
void showDisableLanguageToolsPopup() {
if (InstructionsEnum.disableLanguageTools.isToggledOff) {
return;

View file

@ -24,7 +24,6 @@ import 'package:fluffychat/pangea/activity_sessions/activity_session_start/activ
import 'package:fluffychat/pangea/analytics_misc/level_up/star_rain_widget.dart';
import 'package:fluffychat/pangea/chat/widgets/chat_floating_action_button.dart';
import 'package:fluffychat/pangea/chat/widgets/chat_input_bar.dart';
import 'package:fluffychat/pangea/chat/widgets/chat_view_background.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
import 'package:fluffychat/pangea/navigation/navigation_util.dart';
import 'package:fluffychat/utils/account_config.dart';
@ -474,21 +473,11 @@ class ChatView extends StatelessWidget {
// #Pangea
// onTap: controller.clearSingleSelectedEvent,
// child: ChatEventList(controller: controller),
child: Stack(
children: [
ListenableBuilder(
listenable:
controller.timelineUpdateNotifier,
builder: (context, _) {
return ChatEventList(
controller: controller,
);
},
),
ChatViewBackground(
controller.choreographer.itController.open,
),
],
child: ListenableBuilder(
listenable: controller.timelineUpdateNotifier,
builder: (context, _) {
return ChatEventList(controller: controller);
},
),
// Pangea#
),

View file

@ -8,7 +8,6 @@ import 'package:slugify/slugify.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/choreographer/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/choreo_mode_enum.dart';
import 'package:fluffychat/pangea/choreographer/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/igc/pangea_match_state_model.dart';
import 'package:fluffychat/pangea/choreographer/text_editing/pangea_text_controller.dart';
@ -484,7 +483,6 @@ class InputBar extends StatelessWidget {
contextMenuBuilder: (c, e) =>
markdownContextBuilder(c, e, controller!),
onTap: () => _onInputTap(context),
readOnly: choreographer.choreoMode == ChoreoModeEnum.it,
autocorrect: MatrixState.pangeaController.userController
.isToolEnabled(ToolSetting.enableAutocorrect),
// Pangea#
@ -532,9 +530,7 @@ class InputBar extends StatelessWidget {
builder: (context, _) => SizedBox(
height: 24,
child: ShrinkableText(
text: choreographer.itController.open.value
? L10n.of(context).buildTranslation
: _defaultHintText(context),
text: _defaultHintText(context),
maxWidth: double.infinity,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).disabledColor,

View file

@ -1,43 +0,0 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
class ActivityRoleTooltip extends StatelessWidget {
final Room room;
final ValueNotifier<bool> hide;
const ActivityRoleTooltip({
required this.room,
required this.hide,
super.key,
});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: hide,
builder: (context, hide, _) {
if (!room.showActivityChatUI || room.ownRole?.goal == null || hide) {
return const SizedBox();
}
return InlineTooltip(
message: room.ownRole!.goal!,
isClosed: room.hasDismissedGoalTooltip,
onClose: () async {
await room.dismissGoalTooltip();
},
padding: EdgeInsets.only(
left: 16.0,
right: 16.0,
top: FluffyThemes.isColumnMode(context) ? 16.0 : 8.0,
),
);
},
);
}
}

View file

@ -16,7 +16,6 @@ class ChatFloatingActionButton extends StatelessWidget {
return ListenableBuilder(
listenable: Listenable.merge([
controller.choreographer.errorService,
controller.choreographer.itController.open,
controller.scrollController,
controller.scrollableNotifier,
]),
@ -31,8 +30,7 @@ class ChatFloatingActionButton extends StatelessWidget {
);
}
if (controller.choreographer.errorService.error != null &&
!controller.choreographer.itController.open.value) {
if (controller.choreographer.errorService.error != null) {
return ChoreographerHasErrorButton(
controller.choreographer.errorService.error!,
controller.choreographer,

View file

@ -5,10 +5,8 @@ import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/chat.dart';
import 'package:fluffychat/pages/chat/chat_emoji_picker.dart';
import 'package:fluffychat/pages/chat/reply_display.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_session_chat/activity_role_tooltip.dart';
import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart';
import 'package:fluffychat/pangea/chat/widgets/pangea_chat_input_row.dart';
import 'package:fluffychat/pangea/choreographer/it/it_bar.dart';
import 'package:fluffychat/pangea/instructions/instructions_enum.dart';
import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart';
class ChatInputBar extends StatelessWidget {
@ -27,36 +25,27 @@ class ChatInputBar extends StatelessWidget {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ValueListenableBuilder(
valueListenable: controller.choreographer.itController.open,
builder: (context, open, _) {
return open
? Container(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.maxTimelineWidth,
),
alignment: Alignment.center,
child: InstructionsInlineTooltip(
instructionsEnum: InstructionsEnum.clickBestOption,
animate: false,
padding: EdgeInsets.only(
left: 16.0,
right: 16.0,
top: FluffyThemes.isColumnMode(context) ? 16.0 : 8.0,
),
),
)
: Container(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.maxTimelineWidth,
),
alignment: Alignment.center,
child: ActivityRoleTooltip(
room: controller.room,
hide: controller.choreographer.itController.open,
),
);
},
Container(
constraints: const BoxConstraints(
maxWidth: FluffyThemes.maxTimelineWidth,
),
alignment: Alignment.center,
child:
controller.room.showActivityChatUI &&
controller.room.ownRole?.goal != null
? InlineTooltip(
message: controller.room.ownRole!.goal!,
isClosed: controller.room.hasDismissedGoalTooltip,
onClose: () async {
await controller.room.dismissGoalTooltip();
},
padding: EdgeInsets.only(
left: 16.0,
right: 16.0,
top: FluffyThemes.isColumnMode(context) ? 16.0 : 8.0,
),
)
: SizedBox(),
),
Container(
margin: EdgeInsets.all(
@ -75,7 +64,6 @@ class ChatInputBar extends StatelessWidget {
: Column(
mainAxisSize: MainAxisSize.min,
children: [
ITBar(choreographer: controller.choreographer),
ReplyDisplay(controller),
PangeaChatInputRow(controller: controller),
ChatEmojiPicker(controller),

View file

@ -1,38 +0,0 @@
import 'dart:ui';
import 'package:flutter/material.dart';
class ChatViewBackground extends StatelessWidget {
final ValueNotifier<bool> visible;
const ChatViewBackground(this.visible, {super.key});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: visible,
builder: (context, value, _) {
return value
? Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: Material(
borderOnForeground: false,
color: const Color.fromRGBO(0, 0, 0, 1).withAlpha(150),
clipBehavior: Clip.antiAlias,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 2.5, sigmaY: 2.5),
child: Container(
height: double.infinity,
width: double.infinity,
color: Colors.transparent,
),
),
),
)
: const SizedBox.shrink();
},
);
}
}

View file

@ -69,15 +69,7 @@ class PangeaChatInputRow extends StatelessWidget {
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
height: height,
width:
text.text.isEmpty &&
!controller
.choreographer
.itController
.open
.value
? height
: 0,
width: text.text.isEmpty ? height : 0,
alignment: Alignment.center,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
@ -298,12 +290,7 @@ class PangeaChatInputRow extends StatelessWidget {
alignment: Alignment.center,
child:
PlatformInfos.platformCanRecord &&
text.text.isEmpty &&
!controller
.choreographer
.itController
.open
.value
text.text.isEmpty
? IconButton(
tooltip: L10n.of(context).voiceMessage,
onPressed: () => recordingViewModel

View file

@ -1,13 +1,4 @@
import 'package:flutter/material.dart';
class ChoreoConstants {
static const numberOfITChoices = 4;
static const levelThresholdForGreen = 1;
static const levelThresholdForYellow = 2;
static const levelThresholdForRed = 3;
static const green = Colors.green;
static const yellow = Color.fromARGB(255, 206, 152, 2);
static const red = Colors.red;
static const int msBeforeIGCStart = 10000;
static const int maxLength = 1000;
static const String inputTransformTargetKey = 'input_text_field';

View file

@ -1 +0,0 @@
enum ChoreoModeEnum { igc, it }

View file

@ -4,7 +4,7 @@ import 'package:fluffychat/pangea/choreographer/choreo_edit_model.dart';
import 'package:fluffychat/pangea/choreographer/igc/pangea_match_model.dart';
import 'package:fluffychat/pangea/choreographer/igc/pangea_match_status_enum.dart';
import 'package:fluffychat/pangea/choreographer/igc/span_data_model.dart';
import 'it/completed_it_step_model.dart';
import 'completed_it_step_model.dart';
/// this class lives within a [PangeaIGCEvent]
/// it always has a [RepresentationEvent] parent

View file

@ -6,46 +6,37 @@ import 'package:async/async.dart';
import 'package:fluffychat/pangea/choreographer/assistance_state_enum.dart';
import 'package:fluffychat/pangea/choreographer/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/choreo_mode_enum.dart';
import 'package:fluffychat/pangea/choreographer/choreo_record_model.dart';
import 'package:fluffychat/pangea/choreographer/choreographer_state_extension.dart';
import 'package:fluffychat/pangea/choreographer/igc/igc_controller.dart';
import 'package:fluffychat/pangea/choreographer/igc/pangea_match_state_model.dart';
import 'package:fluffychat/pangea/choreographer/igc/pangea_match_status_enum.dart';
import 'package:fluffychat/pangea/choreographer/it/completed_it_step_model.dart';
import 'package:fluffychat/pangea/choreographer/pangea_message_content_model.dart';
import 'package:fluffychat/pangea/choreographer/text_editing/edit_type_enum.dart';
import 'package:fluffychat/pangea/choreographer/text_editing/pangea_text_controller.dart';
import 'package:fluffychat/pangea/events/models/representation_content_model.dart';
import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart';
import 'package:fluffychat/pangea/events/repo/token_api_models.dart';
import 'package:fluffychat/pangea/events/repo/tokens_repo.dart';
import 'package:fluffychat/pangea/languages/language_constants.dart';
import 'package:fluffychat/pangea/learning_settings/tool_settings_enum.dart';
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/text_to_speech/tts_controller.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import '../../widgets/matrix.dart';
import 'choreographer_error_controller.dart';
import 'it/it_controller.dart';
class Choreographer extends ChangeNotifier {
final FocusNode inputFocus;
late final PangeaTextController textController;
late final ITController itController;
late final IgcController igcController;
late final ChoreographerErrorController errorService;
ChoreoRecordModel? _choreoRecord;
final ValueNotifier<bool> _isFetching = ValueNotifier(false);
final ValueNotifier<int> _timesDismissedIT = ValueNotifier(0);
int _timesClicked = 0;
Timer? _debounceTimer;
String? _lastChecked;
ChoreoModeEnum _choreoMode = ChoreoModeEnum.igc;
DateTime? _lastIgcError;
DateTime? _lastTokensError;
@ -54,17 +45,13 @@ class Choreographer extends ChangeNotifier {
StreamSubscription? _languageSub;
StreamSubscription? _settingsUpdateSub;
StreamSubscription? _acceptedContinuanceSub;
StreamSubscription? _updatedMatchSub;
Choreographer(this.inputFocus) {
_initialize();
}
int get timesClicked => _timesClicked;
ValueNotifier<bool> get isFetching => _isFetching;
ValueNotifier<int> get timesDismissedIT => _timesDismissedIT;
ChoreoModeEnum get choreoMode => _choreoMode;
String get currentText => textController.text;
ChoreoRecordModel get _record => _choreoRecord ??= ChoreoRecordModel(
@ -86,12 +73,6 @@ class Choreographer extends ChangeNotifier {
errorService = ChoreographerErrorController();
errorService.addListener(notifyListeners);
itController = ITController(
(e) => errorService.setErrorAndLock(ChoreoError(raw: e)),
);
itController.open.addListener(_onUpdateITOpenStatus);
itController.editing.addListener(_onSubmitSourceTextEdits);
igcController = IgcController(
(e) {
errorService.setErrorAndLock(ChoreoError(raw: e));
@ -121,9 +102,6 @@ class Choreographer extends ChangeNotifier {
notifyListeners();
});
_acceptedContinuanceSub ??= itController.acceptedContinuanceStream.stream
.listen(_onAcceptContinuance);
_updatedMatchSub ??= igcController.matchUpdateStream.stream.listen(
_onUpdateMatch,
);
@ -131,36 +109,27 @@ class Choreographer extends ChangeNotifier {
void clear() {
_lastChecked = null;
_timesClicked = 0;
_isFetching.value = false;
_choreoRecord = null;
itController.closeIT();
itController.clearSourceText();
itController.clearSession();
igcController.clear();
_resetDebounceTimer();
_setChoreoMode(ChoreoModeEnum.igc);
notifyListeners();
}
@override
void dispose() {
errorService.removeListener(notifyListeners);
itController.open.removeListener(_onCloseIT);
itController.editing.removeListener(_onSubmitSourceTextEdits);
textController.removeListener(_onChange);
_languageSub?.cancel();
_settingsUpdateSub?.cancel();
_acceptedContinuanceSub?.cancel();
_updatedMatchSub?.cancel();
_debounceTimer?.cancel();
igcController.dispose();
itController.dispose();
errorService.dispose();
textController.dispose();
_isFetching.dispose();
_timesDismissedIT.dispose();
TtsController.stop();
super.dispose();
@ -168,23 +137,6 @@ class Choreographer extends ChangeNotifier {
void onPaste(String value) => _record.pastedStrings.add(value);
void onClickSend() {
if (assistanceState == AssistanceStateEnum.fetched) {
_timesClicked++;
// if user is doing IT, call closeIT here to
// ensure source text is replaced when needed
if (itController.open.value && _timesClicked > 1) {
itController.closeIT(dismiss: true);
}
}
}
void _setChoreoMode(ChoreoModeEnum mode) {
_choreoMode = mode;
notifyListeners();
}
void _resetDebounceTimer() {
if (_debounceTimer != null) {
_debounceTimer?.cancel();
@ -228,10 +180,8 @@ class Choreographer extends ChangeNotifier {
_lastChecked = textController.text;
if (errorService.isError) return;
if (textController.editType == EditTypeEnum.keyboard) {
if (igcController.currentText != null ||
itController.sourceText.value != null) {
if (igcController.currentText != null) {
igcController.clear();
itController.clearSourceText();
notifyListeners();
}
@ -254,9 +204,7 @@ class Choreographer extends ChangeNotifier {
MatrixState.pangeaController.userController.userL1 == null ||
(!ToolSetting.interactiveGrammar.enabled &&
!ToolSetting.interactiveTranslator.enabled) ||
(!ToolSetting.autoIGC.enabled &&
!manual &&
_choreoMode != ChoreoModeEnum.it) ||
(!ToolSetting.autoIGC.enabled && !manual) ||
_backoffRequest(_lastIgcError, _igcErrorBackoff)) {
return;
}
@ -334,20 +282,9 @@ class Choreographer extends ChangeNotifier {
tokensResp = res.isValue ? res.result : null;
}
final hasOriginalWritten =
_record.includedIT && itController.sourceText.value != null;
return PangeaMessageContentModel(
message: message,
choreo: _record,
originalWritten: hasOriginalWritten
? PangeaRepresentation(
langCode: l1LangCode ?? LanguageKeys.unknownLanguage,
text: itController.sourceText.value!,
originalWritten: true,
originalSent: false,
)
: null,
tokensSent: tokensResp != null
? PangeaMessageTokens(
tokens: tokensResp.tokens,
@ -357,60 +294,6 @@ class Choreographer extends ChangeNotifier {
);
}
void _onUpdateITOpenStatus() {
itController.open.value ? _onOpenIT() : _onCloseIT();
notifyListeners();
}
void _onOpenIT() {
inputFocus.unfocus();
final itMatch = igcController.openMatches.firstWhere(
(match) => match.updatedMatch.isITStart,
orElse: () =>
throw Exception("Attempted to open IT without an ITStart match"),
);
igcController.clear();
itMatch.setStatus(PangeaMatchStatusEnum.accepted);
_record.addRecord("", match: itMatch.updatedMatch);
_setChoreoMode(ChoreoModeEnum.it);
textController.setSystemText("", EditTypeEnum.it);
}
void _onCloseIT() {
if (itController.dismissed &&
currentText.isEmpty &&
itController.sourceText.value != null) {
textController.setSystemText(
itController.sourceText.value!,
EditTypeEnum.itDismissed,
);
}
if (itController.dismissed) {
_timesDismissedIT.value = _timesDismissedIT.value + 1;
}
_setChoreoMode(ChoreoModeEnum.igc);
errorService.resetError();
}
void _onSubmitSourceTextEdits() {
if (itController.editing.value) return;
textController.setSystemText("", EditTypeEnum.it);
}
void _onAcceptContinuance(CompletedITStepModel step) {
textController.setSystemText(
textController.text + step.continuances[step.chosen].text,
EditTypeEnum.it,
);
_record.addRecord(textController.text, step: step);
inputFocus.requestFocus();
notifyListeners();
}
void clearMatches(Object error) {
MatrixState.pAnyState.closeAllOverlays();
igcController.clearMatches();

View file

@ -8,11 +8,6 @@ class ChoreographerSendButton extends StatelessWidget {
final ChatController controller;
const ChoreographerSendButton({super.key, required this.controller});
Future<void> _onPressed(BuildContext context) async {
controller.choreographer.onClickSend();
controller.onInputBarSubmitted();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
@ -26,7 +21,7 @@ class ChoreographerSendButton extends StatelessWidget {
color: controller.choreographer.assistanceState.sendButtonColor(
context,
),
onPressed: fetching ? null : () => _onPressed(context),
onPressed: fetching ? null : controller.onInputBarSubmitted,
tooltip: L10n.of(context).send,
),
);

View file

@ -1,5 +1,4 @@
import 'package:fluffychat/pangea/choreographer/assistance_state_enum.dart';
import 'package:fluffychat/pangea/choreographer/choreo_mode_enum.dart';
import 'package:fluffychat/pangea/choreographer/choreographer.dart';
import 'package:fluffychat/widgets/matrix.dart';
@ -8,7 +7,7 @@ extension ChoregrapherStateExtension on Choreographer {
final isSubscribed =
MatrixState.pangeaController.subscriptionController.isSubscribed;
if (isSubscribed == false) return AssistanceStateEnum.noSub;
if (currentText.trim().isEmpty && itController.sourceText.value == null) {
if (currentText.trim().isEmpty) {
return AssistanceStateEnum.noMessage;
}
@ -16,15 +15,12 @@ extension ChoregrapherStateExtension on Choreographer {
return AssistanceStateEnum.error;
}
if (igcController.openMatches.isNotEmpty ||
(choreoMode == ChoreoModeEnum.it &&
itController.currentITStep.value?.isFinal != true)) {
if (igcController.openMatches.isNotEmpty) {
return AssistanceStateEnum.fetched;
}
if (isFetching.value) return AssistanceStateEnum.fetching;
if (igcController.currentText == null &&
itController.sourceText.value == null) {
if (igcController.currentText == null) {
return AssistanceStateEnum.notFetched;
}
return AssistanceStateEnum.complete;

View file

@ -1,8 +1,3 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import '../choreo_constants.dart';
class CompletedITStepModel {
final List<ContinuanceModel> continuances;
final int chosen;
@ -84,58 +79,6 @@ class ContinuanceModel {
return data;
}
ContinuanceModel copyWith({
double? probability,
int? level,
String? text,
String? description,
int? indexSavedByServer,
bool? wasClicked,
bool? inDictionary,
bool? hasInfo,
bool? gold,
}) {
return ContinuanceModel(
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) ||

View file

@ -1,96 +0,0 @@
import 'dart:convert';
import 'package:async/async.dart';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/choreographer/igc/span_data_model.dart';
import 'package:fluffychat/pangea/choreographer/igc/span_data_request.dart';
import 'package:fluffychat/pangea/choreographer/igc/span_data_response.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';
class _SpanDetailsCacheItem {
final Future<SpanData> data;
final DateTime timestamp;
const _SpanDetailsCacheItem({required this.data, required this.timestamp});
}
class SpanDataRepo {
static final Map<String, _SpanDetailsCacheItem> _cache = {};
static const Duration _cacheDuration = Duration(minutes: 10);
static Future<Result<SpanData>> get(
String? accessToken, {
required SpanDetailsRequest request,
}) async {
final cached = _getCached(request);
if (cached != null) {
return _getResult(request, cached);
}
final future = _fetch(accessToken, request: request);
_setCached(request, future);
return _getResult(request, future);
}
static Future<SpanData> _fetch(
String? accessToken, {
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(),
);
if (res.statusCode != 200) {
throw Exception('Failed to load span details');
}
final respModel = SpanDetailsResponse.fromJson(
jsonDecode(utf8.decode(res.bodyBytes)),
);
return respModel.span;
}
static Future<Result<SpanData>> _getResult(
SpanDetailsRequest request,
Future<SpanData> future,
) async {
try {
final res = await future;
return Result.value(res);
} catch (e, s) {
_cache.remove(request.hashCode.toString());
ErrorHandler.logError(e: e, s: s, data: request.toJson());
return Result.error(e);
}
}
static Future<SpanData>? _getCached(SpanDetailsRequest request) {
final cacheKeys = [..._cache.keys];
for (final key in cacheKeys) {
if (DateTime.now().difference(_cache[key]!.timestamp) >= _cacheDuration) {
_cache.remove(key);
}
}
return _cache[request.hashCode.toString()]?.data;
}
static void _setCached(
SpanDetailsRequest request,
Future<SpanData> response,
) {
_cache[request.hashCode.toString()] = _SpanDetailsCacheItem(
data: response,
timestamp: DateTime.now(),
);
}
}

View file

@ -1,47 +0,0 @@
import 'package:fluffychat/pangea/choreographer/igc/span_data_model.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,
ModelKey.enableIT: enableIT,
ModelKey.enableIGC: 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;
}
}

View file

@ -1,26 +0,0 @@
import 'package:fluffychat/pangea/choreographer/igc/span_data_model.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']),
);
}

View file

@ -1,92 +0,0 @@
import 'dart:convert';
import 'package:async/async.dart';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/choreographer/it/contextual_definition_request_model.dart';
import 'package:fluffychat/pangea/choreographer/it/contextual_definition_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';
class ContextualDefinitionRepo {
static final Map<String, Future<String>> _cache = {};
static Future<Result<String>> get(
String accessToken,
ContextualDefinitionRequestModel request,
) async {
final cached = _getCached(request);
if (cached != null) {
try {
return Result.value(await cached);
} catch (e, s) {
_cache.remove(request.hashCode.toString());
ErrorHandler.logError(e: e, s: s, data: request.toJson());
return Result.error(e);
}
}
final future = _fetch(accessToken, request);
_setCached(request, future);
return _getResult(request, future);
}
static Future<String> _fetch(
String accessToken,
ContextualDefinitionRequestModel request,
) async {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: accessToken,
);
final Response res = await req.post(
url: PApiUrls.contextualDefinition,
body: request.toJson(),
);
if (res.statusCode != 200) {
throw Exception(
"Contextual definition request failed with status code ${res.statusCode}",
);
}
final ContextualDefinitionResponseModel response =
ContextualDefinitionResponseModel.fromJson(
jsonDecode(utf8.decode(res.bodyBytes).toString()),
);
if (response.text.isEmpty) {
ErrorHandler.logError(
e: Exception("empty text in contextual definition response"),
data: {"request": request.toJson(), "accessToken": accessToken},
);
}
return response.text;
}
static Future<Result<String>> _getResult(
ContextualDefinitionRequestModel request,
Future<String> future,
) async {
try {
final res = await future;
return Result.value(res);
} catch (e, s) {
_cache.remove(request.hashCode.toString());
ErrorHandler.logError(e: e, s: s, data: request.toJson());
return Result.error(e);
}
}
static Future<String>? _getCached(ContextualDefinitionRequestModel request) =>
_cache[request.hashCode.toString()];
static void _setCached(
ContextualDefinitionRequestModel request,
Future<String> response,
) => _cache[request.hashCode.toString()] = response;
}

View file

@ -1,44 +0,0 @@
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
class ContextualDefinitionRequestModel {
final String fullText;
final String word;
final String feedbackLang;
final String fullTextLang;
final String wordLang;
const ContextualDefinitionRequestModel({
required this.fullText,
required this.word,
required this.feedbackLang,
required this.fullTextLang,
required this.wordLang,
});
Map<String, dynamic> toJson() => {
ModelKey.fullText: fullText,
ModelKey.word: word,
ModelKey.lang: feedbackLang,
ModelKey.fullTextLang: fullTextLang,
ModelKey.wordLang: wordLang,
};
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ContextualDefinitionRequestModel &&
runtimeType == other.runtimeType &&
fullText == other.fullText &&
word == other.word &&
feedbackLang == other.feedbackLang &&
fullTextLang == other.fullTextLang &&
wordLang == other.wordLang;
@override
int get hashCode =>
fullText.hashCode ^
word.hashCode ^
feedbackLang.hashCode ^
fullTextLang.hashCode ^
wordLang.hashCode;
}

View file

@ -1,9 +0,0 @@
class ContextualDefinitionResponseModel {
final String text;
const ContextualDefinitionResponseModel({required this.text});
factory ContextualDefinitionResponseModel.fromJson(
Map<String, dynamic> json,
) => ContextualDefinitionResponseModel(text: json["response"]);
}

View file

@ -1,37 +0,0 @@
import 'package:fluffychat/pangea/choreographer/it/completed_it_step_model.dart';
class GoldRouteTrackerModel {
final String _originalText;
final List<ContinuanceModel> continuances;
const GoldRouteTrackerModel(this.continuances, String originalText)
: _originalText = originalText;
ContinuanceModel? 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,410 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_summary/animated_progress_bar.dart';
import 'package:fluffychat/pangea/choreographer/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/choreographer.dart';
import 'package:fluffychat/pangea/choreographer/it/completed_it_step_model.dart';
import 'package:fluffychat/pangea/choreographer/it/it_feedback_card.dart';
import 'package:fluffychat/pangea/choreographer/it/word_data_card.dart';
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
import 'package:fluffychat/pangea/common/widgets/error_indicator.dart';
import 'package:fluffychat/pangea/learning_settings/settings_learning.dart';
import 'package:fluffychat/pangea/translation/full_text_translation_request_model.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../common/utils/overlay.dart';
import '../../common/widgets/choice_array.dart';
class ITBar extends StatefulWidget {
final Choreographer choreographer;
const ITBar({super.key, required this.choreographer});
@override
ITBarState createState() => ITBarState();
}
class ITBarState extends State<ITBar> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
final TextEditingController _sourceTextController = TextEditingController();
Timer? _successTimer;
bool _visible = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
_openListener();
_open.addListener(_openListener);
}
@override
void dispose() {
_controller.dispose();
_sourceTextController.dispose();
_successTimer?.cancel();
_open.removeListener(_openListener);
super.dispose();
}
FullTextTranslationRequestModel _translationRequest(String text) =>
FullTextTranslationRequestModel(
text: text,
tgtLang: MatrixState.pangeaController.userController.userL1!.langCode,
userL1: MatrixState.pangeaController.userController.userL1!.langCode,
userL2: MatrixState.pangeaController.userController.userL2!.langCode,
);
void _openListener() {
if (!mounted) return;
final nextText = _sourceText.value ?? widget.choreographer.currentText;
if (_sourceTextController.text != nextText) {
_sourceTextController.text = nextText;
}
if (_open.value) {
setState(() => _visible = true);
_controller.forward();
} else {
_controller.reverse().then((value) {
if (!mounted) return;
setState(() => _visible = false);
});
}
}
ValueNotifier<String?> get _sourceText =>
widget.choreographer.itController.sourceText;
ValueNotifier<bool> get _open => widget.choreographer.itController.open;
void _showFeedbackCard(
ContinuanceModel continuance, [
Color? borderColor,
bool selected = false,
]) {
final text = continuance.text;
MatrixState.pAnyState.closeOverlay("it_feedback_card");
OverlayUtil.showPositionedCard(
context: context,
cardToShow: selected
? WordDataCard(
word: text,
langCode:
MatrixState.pangeaController.userController.userL2!.langCode,
fullText: _sourceText.value ?? widget.choreographer.currentText,
)
: ITFeedbackCard(_translationRequest(text)),
maxHeight: 300,
maxWidth: 300,
borderColor: borderColor,
transformTargetId: 'it_bar',
isScrollable: false,
overlayKey: "it_feedback_card",
ignorePointer: true,
);
}
void _selectContinuance(int index) {
MatrixState.pAnyState.closeOverlay("it_feedback_card");
ContinuanceModel continuance;
try {
continuance = widget.choreographer.itController.selectContinuance(index);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
level: SentryLevel.warning,
data: {"index": index},
);
widget.choreographer.itController.closeIT();
return;
}
if (continuance.level == 1) {
_onCorrectSelection(index);
} else {
_showFeedbackCard(
continuance,
continuance.level == 2 ? ChoreoConstants.yellow : ChoreoConstants.red,
true,
);
}
}
void _onCorrectSelection(int index) {
_successTimer?.cancel();
_successTimer = Timer(const Duration(milliseconds: 500), () {
if (!mounted) return;
try {
widget.choreographer.itController.acceptContinuance(index);
} catch (e, s) {
ErrorHandler.logError(
e: e,
s: s,
level: SentryLevel.warning,
data: {"index": index},
);
widget.choreographer.itController.closeIT();
}
});
}
@override
Widget build(BuildContext context) {
if (!_visible) {
return const SizedBox.shrink();
}
return AnimatedBuilder(
animation: _animation,
builder: (context, child) => SizeTransition(
sizeFactor: _animation,
axisAlignment: -1.0,
child: child,
),
child: CompositedTransformTarget(
link: MatrixState.pAnyState.layerLinkAndKey('it_bar').link,
child: Container(
key: MatrixState.pAnyState.layerLinkAndKey('it_bar').key,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
color: Theme.of(context).colorScheme.surfaceContainer,
),
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 12.0,
children: [
_ITBarHeader(
onClose: () =>
widget.choreographer.itController.closeIT(dismiss: true),
setEditing:
widget.choreographer.itController.setEditingSourceText,
editing: widget.choreographer.itController.editing,
progress: widget.choreographer.itController.progress,
sourceTextController: _sourceTextController,
sourceText: _sourceText,
onSubmitEdits: (_) {
widget.choreographer.itController.submitSourceTextEdits(
_sourceTextController.text,
);
},
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
constraints: const BoxConstraints(minHeight: 80),
child: Center(
child: widget.choreographer.errorService.isError
? Row(
spacing: 8.0,
mainAxisSize: MainAxisSize.min,
children: [
ErrorIndicator(
message: L10n.of(context).translationError,
style: TextStyle(
fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.error,
),
),
IconButton(
onPressed:
widget.choreographer.itController.closeIT,
icon: const Icon(Icons.close, size: 20),
),
],
)
: ValueListenableBuilder(
valueListenable:
widget.choreographer.itController.currentITStep,
builder: (context, step, _) {
return step == null
? CircularProgressIndicator(
strokeWidth: 2.0,
color: Theme.of(
context,
).colorScheme.primary,
)
: _ITChoices(
continuances: step.continuances,
onPressed: _selectContinuance,
onLongPressed: _showFeedbackCard,
);
},
),
),
),
],
),
),
),
);
}
}
class _ITBarHeader extends StatelessWidget {
final VoidCallback onClose;
final Function(String) onSubmitEdits;
final Function(bool) setEditing;
final ValueNotifier<bool> editing;
final ValueNotifier<double> progress;
final TextEditingController sourceTextController;
final ValueNotifier<String?> sourceText;
const _ITBarHeader({
required this.onClose,
required this.setEditing,
required this.editing,
required this.progress,
required this.onSubmitEdits,
required this.sourceTextController,
required this.sourceText,
});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: editing,
builder: (context, isEditing, _) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedCrossFade(
duration: const Duration(milliseconds: 200),
crossFadeState: isEditing
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: Row(
spacing: 12.0,
children: [
Expanded(
child: TextField(
controller: sourceTextController,
autofocus: true,
enableSuggestions: false,
maxLines: null,
textInputAction: TextInputAction.send,
onSubmitted: onSubmitEdits,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
),
IconButton(
color: Theme.of(context).colorScheme.primary,
icon: const Icon(Icons.close_outlined),
onPressed: () => setEditing(false),
),
],
),
secondChild: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: ValueListenableBuilder(
valueListenable: progress,
builder: (context, value, _) => AnimatedProgressBar(
height: 20.0,
widthPercent: value,
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
barColor: Theme.of(
context,
).colorScheme.primary.withAlpha(180),
),
),
),
),
IconButton(
color: Theme.of(context).colorScheme.primary,
onPressed: () => setEditing(true),
icon: const Icon(Icons.edit_outlined),
),
IconButton(
color: Theme.of(context).colorScheme.primary,
icon: const Icon(Icons.settings_outlined),
onPressed: () => showDialog(
context: context,
builder: (c) => const SettingsLearning(),
barrierDismissible: false,
),
),
IconButton(
color: Theme.of(context).colorScheme.primary,
icon: const Icon(Icons.close_outlined),
onPressed: onClose,
),
],
),
),
isEditing
? const SizedBox(height: 24.0)
: ValueListenableBuilder(
valueListenable: sourceText,
builder: (context, text, _) {
return Container(
padding: const EdgeInsets.only(top: 8.0),
constraints: const BoxConstraints(minHeight: 24.0),
child: sourceText.value != null
? Text(
sourceText.value!,
textAlign: TextAlign.center,
)
: const SizedBox(),
);
},
),
],
);
},
);
}
}
class _ITChoices extends StatelessWidget {
final List<ContinuanceModel> continuances;
final Function(int) onPressed;
final Function(ContinuanceModel) onLongPressed;
const _ITChoices({
required this.continuances,
required this.onPressed,
required this.onLongPressed,
});
@override
Widget build(BuildContext context) {
return ChoicesArray(
id: Object.hashAll(continuances).toString(),
isLoading: false,
choices: [
...continuances.map(
(e) => Choice(
text: e.text.trim(),
color: e.color,
isGold: e.description == "best",
),
),
],
onPressed: (value, index) => onPressed(index),
onLongPress: (value, index) => onLongPressed(continuances[index]),
selectedChoiceIndex: null,
langCode: MatrixState.pangeaController.userController.userL2Code!,
);
}
}

View file

@ -1,239 +0,0 @@
import 'dart:async';
import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:async/async.dart';
import 'package:fluffychat/pangea/choreographer/it/gold_route_tracker_model.dart';
import 'package:fluffychat/pangea/choreographer/it/it_repo.dart';
import 'package:fluffychat/pangea/choreographer/it/it_response_model.dart';
import 'package:fluffychat/pangea/choreographer/it/it_step_model.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'completed_it_step_model.dart';
import 'it_request_model.dart';
class ITController {
final Function(Object) onError;
final Queue<Completer<ITStepModel>> _queue = Queue();
GoldRouteTrackerModel? _goldRouteTracker;
final ValueNotifier<String?> _sourceText = ValueNotifier(null);
final ValueNotifier<ITStepModel?> _currentITStep = ValueNotifier(null);
final ValueNotifier<bool> _open = ValueNotifier(false);
final ValueNotifier<bool> _editing = ValueNotifier(false);
final ValueNotifier<double> _progress = ValueNotifier(0.0);
ITController(this.onError);
ValueNotifier<bool> get open => _open;
ValueNotifier<bool> get editing => _editing;
ValueNotifier<double> get progress => _progress;
ValueNotifier<ITStepModel?> get currentITStep => _currentITStep;
ValueNotifier<String?> get sourceText => _sourceText;
StreamController<CompletedITStepModel> acceptedContinuanceStream =
StreamController.broadcast();
bool _continuing = false;
bool dismissed = false;
ITRequestModel _request(String textInput) {
assert(_sourceText.value != null);
return ITRequestModel(
text: _sourceText.value!,
customInput: textInput,
sourceLangCode: MatrixState.pangeaController.userController.userL1Code!,
targetLangCode: MatrixState.pangeaController.userController.userL2Code!,
goldTranslation: _goldRouteTracker?.fullTranslation,
goldContinuances: _goldRouteTracker?.continuances,
);
}
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 clearSourceText() {
_sourceText.value = null;
}
void clearSession() {
dismissed = false;
_progress.value = 0.0;
}
void dispose() {
acceptedContinuanceStream.close();
_open.dispose();
_editing.dispose();
_currentITStep.dispose();
_sourceText.dispose();
}
void openIT(String text) {
_sourceText.value = text;
_open.value = true;
_continueIT();
}
void closeIT({bool dismiss = false}) {
MatrixState.pAnyState.closeOverlay("it_feedback_card");
setEditingSourceText(false);
if (dismiss) {
dismissed = true;
}
_open.value = false;
_queue.clear();
_currentITStep.value = null;
_goldRouteTracker = null;
}
void setEditingSourceText(bool value) {
_editing.value = value;
}
void submitSourceTextEdits(String text) {
_queue.clear();
_currentITStep.value = null;
_goldRouteTracker = null;
_progress.value = 0.0;
_sourceText.value = text;
setEditingSourceText(false);
_continueIT();
}
ContinuanceModel selectContinuance(int index) {
if (_currentITStep.value == null) {
throw "onSelectContinuance called when _currentITStep is null";
}
if (index < 0 || index >= _currentITStep.value!.continuances.length) {
throw "onSelectContinuance called with invalid index $index";
}
final currentStep = _currentITStep.value!;
currentStep.continuances[index] = currentStep.continuances[index].copyWith(
wasClicked: true,
);
_currentITStep.value = _currentITStep.value!.copyWith(
continuances: currentStep.continuances,
);
return _currentITStep.value!.continuances[index];
}
void acceptContinuance(int chosenIndex) {
if (_currentITStep.value == null) {
throw "onAcceptContinuance called when _currentITStep is null";
}
if (chosenIndex < 0 ||
chosenIndex >= _currentITStep.value!.continuances.length) {
throw "onAcceptContinuance called with invalid index $chosenIndex";
}
acceptedContinuanceStream.add(
CompletedITStepModel(
_currentITStep.value!.continuances,
chosen: chosenIndex,
),
);
final progress =
(_goldRouteTracker!.continuances.indexWhere(
(c) =>
c.text ==
_currentITStep.value!.continuances[chosenIndex].text,
) +
1) /
_goldRouteTracker!.continuances.length;
_progress.value = progress;
_continueIT();
}
Future<void> _continueIT() async {
if (_continuing) return;
_continuing = true;
try {
if (_currentITStep.value == null) {
await _initTranslationData();
} else if (_queue.isEmpty) {
closeIT();
} else {
final nextStepCompleter = _queue.removeFirst();
_currentITStep.value = await nextStepCompleter.future;
}
} catch (e) {
onError(e);
} finally {
_continuing = false;
}
}
Future<void> _initTranslationData() async {
final res = await _safeRequest("");
if (_sourceText.value == null || !_open.value) return;
if (res.isError || res.result?.goldContinuances == null) {
onError(res.asError!);
return;
}
final result = res.result!;
_goldRouteTracker = GoldRouteTrackerModel(
result.goldContinuances!,
_sourceText.value!,
);
_currentITStep.value = ITStepModel.fromResponse(
sourceText: _sourceText.value!,
currentText: "",
responseModel: res.result!,
storedGoldContinuances: _goldRouteTracker!.continuances,
);
_fillITStepQueue();
}
Future<void> _fillITStepQueue() async {
if (_sourceText.value == null ||
_goldRouteTracker!.continuances.length < 2) {
return;
}
final sourceText = _sourceText.value!;
final goldContinuances = _goldRouteTracker!.continuances;
String currentText = goldContinuances[0].text;
for (int i = 1; i < goldContinuances.length; i++) {
if (_sourceText.value == null || !_open.value) {
return;
}
final completer = Completer<ITStepModel>();
_queue.add(completer);
final resp = await _safeRequest(currentText);
if (resp.isError) {
completer.completeError(resp.asError!);
break;
} else {
final step = ITStepModel.fromResponse(
sourceText: sourceText,
currentText: currentText,
responseModel: resp.result!,
storedGoldContinuances: goldContinuances,
);
completer.complete(step);
}
currentText += goldContinuances[i].text;
}
}
}

View file

@ -1,58 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:async/async.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart';
import 'package:fluffychat/pangea/translation/full_text_translation_repo.dart';
import 'package:fluffychat/pangea/translation/full_text_translation_request_model.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import '../../../widgets/matrix.dart';
import '../../bot/utils/bot_style.dart';
import '../../common/widgets/card_error_widget.dart';
class ITFeedbackCard extends StatelessWidget {
final FullTextTranslationRequestModel req;
const ITFeedbackCard(this.req, {super.key});
Future<Result<String>> _getFeedback() {
return FullTextTranslationRepo.get(
MatrixState.pangeaController.userController.accessToken,
req,
).timeout(
const Duration(seconds: 10),
onTimeout: () => Result.error("Timeout getting translation"),
);
}
@override
Widget build(BuildContext context) {
return FutureBuilder<Result<String>>(
future: _getFeedback(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return CardErrorWidget(L10n.of(context).errorFetchingDefinition);
}
return Container(
constraints: const BoxConstraints(maxWidth: 300),
alignment: Alignment.center,
child: Wrap(
spacing: 10,
alignment: WrapAlignment.center,
children: [
Text(req.text, style: BotStyle.text(context)),
Text("", style: BotStyle.text(context)),
snapshot.hasData
? Text(snapshot.data!.result!, style: BotStyle.text(context))
: TextLoadingShimmer(width: min(140, 10.0 * req.text.length)),
],
),
);
},
);
}
}

View file

@ -1,87 +0,0 @@
import 'dart:convert';
import 'package:async/async.dart';
import 'package:http/http.dart';
import 'package:fluffychat/pangea/common/config/environment.dart';
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 'it_request_model.dart';
import 'it_response_model.dart';
class _ITCacheItem {
final Future<ITResponseModel> response;
final DateTime timestamp;
const _ITCacheItem({required this.response, required this.timestamp});
}
class ITRepo {
static final Map<String, _ITCacheItem> _cache = {};
static const Duration _cacheDuration = Duration(minutes: 10);
static Future<Result<ITResponseModel>> get(ITRequestModel request) {
final cached = _getCached(request);
if (cached != null) {
return _getResult(request, cached);
}
final future = _fetch(request);
_setCached(request, future);
return _getResult(request, future);
}
static Future<ITResponseModel> _fetch(ITRequestModel request) async {
final Requests req = Requests(
choreoApiKey: Environment.choreoApiKey,
accessToken: MatrixState.pangeaController.userController.accessToken,
);
final Response res = await req.post(
url: PApiUrls.firstStep,
body: request.toJson(),
);
if (res.statusCode != 200) {
throw Exception('Failed to load interactive translation');
}
final json = jsonDecode(utf8.decode(res.bodyBytes).toString());
return ITResponseModel.fromJson(json);
}
static Future<Result<ITResponseModel>> _getResult(
ITRequestModel request,
Future<ITResponseModel> future,
) async {
try {
final res = await future;
return Result.value(res);
} catch (e, s) {
_cache.remove(request.hashCode.toString());
ErrorHandler.logError(e: e, s: s, data: request.toJson());
return Result.error(e);
}
}
static Future<ITResponseModel>? _getCached(ITRequestModel request) {
final cacheKeys = [..._cache.keys];
for (final key in cacheKeys) {
if (DateTime.now().difference(_cache[key]!.timestamp) >= _cacheDuration) {
_cache.remove(key);
}
}
return _cache[request.hashCode.toString()]?.response;
}
static void _setCached(
ITRequestModel request,
Future<ITResponseModel> response,
) {
_cache[request.hashCode.toString()] = _ITCacheItem(
response: response,
timestamp: DateTime.now(),
);
}
}

View file

@ -1,69 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:fluffychat/pangea/choreographer/it/completed_it_step_model.dart';
import 'package:fluffychat/pangea/common/constants/model_keys.dart';
class ITRequestModel {
final String text;
final String customInput;
final String sourceLangCode;
final String targetLangCode;
final String? goldTranslation;
final List<ContinuanceModel>? goldContinuances;
const ITRequestModel({
required this.text,
required this.customInput,
required this.sourceLangCode,
required this.targetLangCode,
required this.goldTranslation,
required this.goldContinuances,
});
factory ITRequestModel.fromJson(Map<String, dynamic> json) => ITRequestModel(
text: json[ModelKey.text],
customInput: json['custom_input'],
sourceLangCode: json[ModelKey.srcLang],
targetLangCode: json[ModelKey.tgtLang],
goldTranslation: json[ModelKey.goldTranslation],
goldContinuances: json['gold_continuances'] != null
? (json['gold_continuances'])
.map((e) => ContinuanceModel.fromJson(e))
.toList()
: null,
);
Map<String, dynamic> toJson() => {
ModelKey.text: text,
'custom_input': customInput,
ModelKey.srcLang: sourceLangCode,
ModelKey.tgtLang: targetLangCode,
ModelKey.goldTranslation: goldTranslation,
'gold_continuances': goldContinuances != null
? List.from(goldContinuances!.map((e) => e.toJson()))
: null,
};
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ITRequestModel &&
other.text == text &&
other.customInput == customInput &&
other.sourceLangCode == sourceLangCode &&
other.targetLangCode == targetLangCode &&
other.goldTranslation == goldTranslation &&
listEquals(other.goldContinuances, goldContinuances);
}
@override
int get hashCode =>
text.hashCode ^
customInput.hashCode ^
sourceLangCode.hashCode ^
targetLangCode.hashCode ^
goldTranslation.hashCode ^
Object.hashAll(goldContinuances ?? []);
}

View file

@ -1,74 +0,0 @@
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/choreographer/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/it/completed_it_step_model.dart';
class ITResponseModel {
final String fullTextTranslation;
final List<ContinuanceModel> continuances;
final List<ContinuanceModel>? goldContinuances;
final bool isFinal;
final String? translationId;
final int payloadId;
const ITResponseModel({
required this.fullTextTranslation,
required this.continuances,
required this.translationId,
required this.goldContinuances,
required this.isFinal,
required this.payloadId,
});
factory ITResponseModel.fromJson(Map<String, dynamic> json) {
//PTODO - is continuances a variable type? can we change that?
if (json['continuances'].runtimeType == String) {
debugPrint("continuances was string - ${json['continuances']}");
json['continuances'] = [];
json['finished'] = true;
}
final List<ContinuanceModel> interimCont = (json['continuances'] as List)
.mapIndexed((index, e) {
e["index"] = index;
return ContinuanceModel.fromJson(e);
})
.toList()
.take(ChoreoConstants.numberOfITChoices)
.toList()
.cast<ContinuanceModel>()
//can't do this on the backend because step translation can't filter them out
.where((element) => element.inDictionary)
.toList();
interimCont.shuffle();
return ITResponseModel(
fullTextTranslation: json["full_text_translation"] ?? json["translation"],
continuances: interimCont,
translationId: json['translation_id'],
payloadId: json['payload_id'] ?? 0,
isFinal: json['finished'] ?? false,
goldContinuances: json['gold_continuances'] != null
? (json['gold_continuances'] as Iterable).map((e) {
e["gold"] = true;
return ContinuanceModel.fromJson(e);
}).toList()
: null,
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['full_text_translation'] = fullTextTranslation;
data['continuances'] = continuances.map((v) => v.toJson()).toList();
if (translationId != null) {
data['translation_id'] = translationId;
}
data['payload_id'] = payloadId;
data["finished"] = isFinal;
return data;
}
}

View file

@ -1,38 +0,0 @@
import 'dart:ui';
import 'package:flutter/material.dart';
class ItShimmer extends StatelessWidget {
const ItShimmer({super.key});
@override
Widget build(BuildContext context) {
final color = Theme.of(context).colorScheme.primary.withAlpha(50);
return Wrap(
alignment: WrapAlignment.center,
spacing: 4,
runSpacing: 4,
children: List.generate(3, (_) {
return ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: TextButton(
style: TextButton.styleFrom(
minimumSize: const Size(50, 36),
backgroundColor: color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(horizontal: 7),
),
onPressed: null,
child: const Text(
" ", // 10 spaces
style: TextStyle(color: Colors.transparent, fontSize: 16),
),
),
);
}),
);
}
}

View file

@ -1,61 +0,0 @@
import 'package:fluffychat/pangea/choreographer/choreo_constants.dart';
import 'package:fluffychat/pangea/choreographer/it/completed_it_step_model.dart';
import 'package:fluffychat/pangea/choreographer/it/gold_route_tracker_model.dart';
import 'package:fluffychat/pangea/choreographer/it/it_response_model.dart';
class ITStepModel {
late List<ContinuanceModel> continuances;
late bool isFinal;
ITStepModel({this.continuances = const [], this.isFinal = false});
factory ITStepModel.fromResponse({
required String sourceText,
required String currentText,
required ITResponseModel responseModel,
required List<ContinuanceModel>? storedGoldContinuances,
}) {
final List<ContinuanceModel> gold =
storedGoldContinuances ?? responseModel.goldContinuances ?? [];
final goldTracker = GoldRouteTrackerModel(gold, sourceText);
final isFinal = responseModel.isFinal;
List<ContinuanceModel> continuances;
if (responseModel.continuances.isEmpty) {
continuances = [];
} else {
final ContinuanceModel? 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<ContinuanceModel>.from(responseModel.continuances);
}
}
return ITStepModel(continuances: continuances, isFinal: isFinal);
}
ITStepModel copyWith({List<ContinuanceModel>? continuances, bool? isFinal}) {
return ITStepModel(
continuances: continuances ?? this.continuances,
isFinal: isFinal ?? this.isFinal,
);
}
}

View file

@ -1,69 +0,0 @@
import 'package:flutter/material.dart';
import 'package:async/async.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pangea/bot/utils/bot_style.dart';
import 'package:fluffychat/pangea/choreographer/it/contextual_definition_repo.dart';
import 'package:fluffychat/pangea/choreographer/it/contextual_definition_request_model.dart';
import 'package:fluffychat/pangea/common/widgets/content_loading_indicator.dart';
import 'package:fluffychat/pangea/languages/language_constants.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
class WordDataCard extends StatelessWidget {
final String word;
final String fullText;
final String langCode;
const WordDataCard({
super.key,
required this.word,
required this.fullText,
required this.langCode,
});
ContextualDefinitionRequestModel get _request =>
ContextualDefinitionRequestModel(
fullText: fullText,
word: word,
fullTextLang: langCode,
wordLang: langCode,
feedbackLang:
MatrixState.pangeaController.userController.userL1Code ??
LanguageKeys.defaultLanguage,
);
Future<Result<String>> _fetchDefinition() {
return ContextualDefinitionRepo.get(
MatrixState.pangeaController.userController.accessToken,
_request,
).timeout(
const Duration(seconds: 10),
onTimeout: () => Result.error("Timeout getting definition"),
);
}
@override
Widget build(BuildContext context) {
return Center(
child: FutureBuilder<Result<String>>(
future: _fetchDefinition(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const ContentLoadingIndicator();
}
final result = snapshot.data!;
if (result.isError) {
return Text(
L10n.of(context).sorryNoResults,
style: BotStyle.text(context),
textAlign: TextAlign.center,
);
}
return Text(result.result!, style: BotStyle.text(context));
},
),
);
}
}

View file

@ -31,20 +31,13 @@ class PApiUrls {
"${PApiUrls._choreoEndpoint}/language_detection";
static String igcLite = "${PApiUrls._choreoEndpoint}/grammar_v2";
static String spanDetails = "${PApiUrls._choreoEndpoint}/span_details";
static String simpleTranslation =
"${PApiUrls._choreoEndpoint}/translation/direct";
static String tokenize = "${PApiUrls._choreoEndpoint}/tokenize";
static String contextualDefinition =
"${PApiUrls._choreoEndpoint}/contextual_definition";
static String firstStep = "${PApiUrls._choreoEndpoint}/it_initialstep";
static String textToSpeech = "${PApiUrls._choreoEndpoint}/text_to_speech";
static String speechToText = "${PApiUrls._choreoEndpoint}/speech_to_text";
static String phoneticTranscription =
"${PApiUrls._choreoEndpoint}/phonetic_transcription";
static String phoneticTranscriptionV2 =
"${PApiUrls._choreoEndpoint}/phonetic_transcription_v2";
@ -55,15 +48,6 @@ class PApiUrls {
"${PApiUrls._choreoEndpoint}/lemma_definition";
static String morphDictionary = "${PApiUrls._choreoEndpoint}/morph_meaning";
// static String activityPlan = "${PApiUrls._choreoEndpoint}/activity_plan";
// static String activityPlanGeneration =
// "${PApiUrls._choreoEndpoint}/activity_plan/generate";
// static String activityPlanSearch =
// "${PApiUrls._choreoEndpoint}/activity_plan/search";
// static String activityModeList = "${PApiUrls._choreoEndpoint}/modes";
// static String objectiveList = "${PApiUrls._choreoEndpoint}/objectives";
// static String topicList = "${PApiUrls._choreoEndpoint}/topics";
static String activitySummary =
"${PApiUrls._choreoEndpoint}/activity_summary";

View file

@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
@ -8,7 +10,6 @@ import 'package:fluffychat/pangea/common/widgets/choice_animation.dart';
import 'package:fluffychat/pangea/text_to_speech/tts_controller.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../bot/utils/bot_style.dart';
import '../../choreographer/it/it_shimmer.dart';
typedef ChoiceCallback = void Function(String value, int index);
@ -46,7 +47,33 @@ class ChoicesArray extends StatelessWidget {
@override
Widget build(BuildContext context) {
return isLoading && (choices == null || choices!.length <= 1)
? const ItShimmer()
? Wrap(
alignment: WrapAlignment.center,
spacing: 4,
runSpacing: 4,
children: List.generate(3, (_) {
return ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: TextButton(
style: TextButton.styleFrom(
minimumSize: const Size(50, 36),
backgroundColor: Theme.of(
context,
).colorScheme.primary.withAlpha(50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(horizontal: 7),
),
onPressed: null,
child: const Text(
" ", // 10 spaces
style: TextStyle(color: Colors.transparent, fontSize: 16),
),
),
);
}),
)
: Wrap(
alignment: WrapAlignment.center,
spacing: 4.0,