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.
		
		
		
		
		
			
		
			
				
	
	
		
			707 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Dart
		
	
			
		
		
	
	
			707 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Dart
		
	
import 'dart:async';
 | 
						|
import 'dart:developer';
 | 
						|
 | 
						|
import 'package:flutter/foundation.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
 | 
						|
import 'package:sentry_flutter/sentry_flutter.dart';
 | 
						|
 | 
						|
import 'package:fluffychat/pages/chat/chat.dart';
 | 
						|
import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.dart';
 | 
						|
import 'package:fluffychat/pangea/choreographer/enums/assistance_state_enum.dart';
 | 
						|
import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart';
 | 
						|
import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart';
 | 
						|
import 'package:fluffychat/pangea/choreographer/models/it_step.dart';
 | 
						|
import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart';
 | 
						|
import 'package:fluffychat/pangea/choreographer/repo/language_detection_repo.dart';
 | 
						|
import 'package:fluffychat/pangea/choreographer/utils/input_paste_listener.dart';
 | 
						|
import 'package:fluffychat/pangea/choreographer/widgets/igc/pangea_text_controller.dart';
 | 
						|
import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart';
 | 
						|
import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart';
 | 
						|
import 'package:fluffychat/pangea/common/utils/any_state_holder.dart';
 | 
						|
import 'package:fluffychat/pangea/common/utils/error_handler.dart';
 | 
						|
import 'package:fluffychat/pangea/common/utils/overlay.dart';
 | 
						|
import 'package:fluffychat/pangea/events/models/pangea_token_model.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/learning_settings/constants/language_constants.dart';
 | 
						|
import 'package:fluffychat/pangea/learning_settings/models/language_model.dart';
 | 
						|
import 'package:fluffychat/pangea/spaces/models/space_model.dart';
 | 
						|
import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart';
 | 
						|
import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart';
 | 
						|
import '../../../widgets/matrix.dart';
 | 
						|
import 'error_service.dart';
 | 
						|
import 'it_controller.dart';
 | 
						|
 | 
						|
enum ChoreoMode { igc, it }
 | 
						|
 | 
						|
class Choreographer {
 | 
						|
  PangeaController pangeaController;
 | 
						|
  ChatController chatController;
 | 
						|
  late PangeaTextController _textController;
 | 
						|
  late ITController itController;
 | 
						|
  late IgcController igc;
 | 
						|
  late ErrorService errorService;
 | 
						|
  late TtsController tts;
 | 
						|
 | 
						|
  bool isFetching = false;
 | 
						|
  int _timesClicked = 0;
 | 
						|
 | 
						|
  final int msBeforeIGCStart = 10000;
 | 
						|
 | 
						|
  Timer? debounceTimer;
 | 
						|
  ChoreoRecord choreoRecord = ChoreoRecord.newRecord;
 | 
						|
  // last checked by IGC or translation
 | 
						|
  String? _lastChecked;
 | 
						|
  ChoreoMode choreoMode = ChoreoMode.igc;
 | 
						|
 | 
						|
  final StreamController stateStream = StreamController.broadcast();
 | 
						|
  StreamSubscription? _trialStream;
 | 
						|
  StreamSubscription? _languageStream;
 | 
						|
  late AssistanceState _currentAssistanceState;
 | 
						|
 | 
						|
  Choreographer(this.pangeaController, this.chatController) {
 | 
						|
    _initialize();
 | 
						|
  }
 | 
						|
  _initialize() {
 | 
						|
    tts = TtsController(chatController: chatController);
 | 
						|
    _textController = PangeaTextController(choreographer: this);
 | 
						|
    InputPasteListener(_textController, onPaste);
 | 
						|
    itController = ITController(this);
 | 
						|
    igc = IgcController(this);
 | 
						|
    errorService = ErrorService(this);
 | 
						|
    _textController.addListener(_onChangeListener);
 | 
						|
    _trialStream = pangeaController
 | 
						|
        .subscriptionController.trialActivationStream.stream
 | 
						|
        .listen((_) => _onChangeListener);
 | 
						|
    _languageStream =
 | 
						|
        pangeaController.userController.stateStream.listen((update) {
 | 
						|
      if (update is Map<String, dynamic> &&
 | 
						|
          update['prev_target_lang'] is LanguageModel) {
 | 
						|
        clear();
 | 
						|
      }
 | 
						|
 | 
						|
      // refresh on any profile update, to account
 | 
						|
      // for changes like enabling autocorrect
 | 
						|
      setState();
 | 
						|
    });
 | 
						|
    _currentAssistanceState = assistanceState;
 | 
						|
    clear();
 | 
						|
  }
 | 
						|
 | 
						|
  void send(BuildContext context) {
 | 
						|
    debugPrint("can send message: $canSendMessage");
 | 
						|
 | 
						|
    // if isFetching, already called to getLanguageHelp and hasn't completed yet
 | 
						|
    // could happen if user clicked send button multiple times in a row
 | 
						|
    if (isFetching) return;
 | 
						|
 | 
						|
    if (igc.igcTextData != null && igc.igcTextData!.matches.isNotEmpty) {
 | 
						|
      igc.showFirstMatch(context);
 | 
						|
      return;
 | 
						|
    } else if (isRunningIT) {
 | 
						|
      // If the user is in the middle of IT, don't send the message.
 | 
						|
      // If they've already clicked the send button once, this will
 | 
						|
      // not be true, so they can still send it if they want.
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    final isSubscribed = pangeaController.subscriptionController.isSubscribed;
 | 
						|
    if (isSubscribed != null && !isSubscribed) {
 | 
						|
      // don't want to run IGC if user isn't subscribed, so either
 | 
						|
      // show the paywall if applicable or just send the message
 | 
						|
      final status = pangeaController.subscriptionController.subscriptionStatus;
 | 
						|
      status == SubscriptionStatus.shouldShowPaywall
 | 
						|
          ? OverlayUtil.showPositionedCard(
 | 
						|
              context: context,
 | 
						|
              cardToShow: PaywallCard(
 | 
						|
                chatController: chatController,
 | 
						|
              ),
 | 
						|
              maxHeight: 325,
 | 
						|
              maxWidth: 325,
 | 
						|
              transformTargetId: inputTransformTargetKey,
 | 
						|
            )
 | 
						|
          : chatController.send();
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (!igc.hasRelevantIGCTextData && !itController.dismissed) {
 | 
						|
      getLanguageHelp().then((value) => _sendWithIGC(context));
 | 
						|
    } else {
 | 
						|
      _sendWithIGC(context);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> _sendWithIGC(BuildContext context) async {
 | 
						|
    if (!canSendMessage) {
 | 
						|
      // It's possible that the reason user can't send message is because they're in the middle of IT. If this is the case,
 | 
						|
      // do nothing (there's no matches to show). The user can click the send button again to override this.
 | 
						|
      if (!isRunningIT) {
 | 
						|
        igc.showFirstMatch(context);
 | 
						|
      }
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    chatController.sendFakeMessage();
 | 
						|
    final PangeaRepresentation? originalWritten =
 | 
						|
        choreoRecord.includedIT && itController.sourceText != null
 | 
						|
            ? PangeaRepresentation(
 | 
						|
                langCode: l1LangCode ?? LanguageKeys.unknownLanguage,
 | 
						|
                text: itController.sourceText!,
 | 
						|
                originalWritten: true,
 | 
						|
                originalSent: false,
 | 
						|
              )
 | 
						|
            : null;
 | 
						|
 | 
						|
    final detectionResp = await LanguageDetectionRepo.get(
 | 
						|
      MatrixState.pangeaController.userController.accessToken,
 | 
						|
      request: LanguageDetectionRequest(
 | 
						|
        text: currentText,
 | 
						|
        senderl1: l1LangCode,
 | 
						|
        senderl2: l2LangCode,
 | 
						|
      ),
 | 
						|
    );
 | 
						|
    final detections = detectionResp.detections;
 | 
						|
    final detectedLanguage =
 | 
						|
        detections.firstOrNull?.langCode ?? LanguageKeys.unknownLanguage;
 | 
						|
 | 
						|
    final PangeaRepresentation originalSent = PangeaRepresentation(
 | 
						|
      langCode: detectedLanguage,
 | 
						|
      text: currentText,
 | 
						|
      originalSent: true,
 | 
						|
      originalWritten: originalWritten == null,
 | 
						|
    );
 | 
						|
 | 
						|
    List<PangeaToken>? res;
 | 
						|
    if (l1LangCode != null && l2LangCode != null) {
 | 
						|
      res = await pangeaController.messageData.getTokens(
 | 
						|
        repEventId: null,
 | 
						|
        room: chatController.room,
 | 
						|
        req: TokensRequestModel(
 | 
						|
          fullText: currentText,
 | 
						|
          langCode: detectedLanguage,
 | 
						|
          senderL1: l1LangCode!,
 | 
						|
          senderL2: l2LangCode!,
 | 
						|
        ),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    final PangeaMessageTokens? tokensSent = res != null
 | 
						|
        ? PangeaMessageTokens(
 | 
						|
            tokens: res,
 | 
						|
            detections: detections,
 | 
						|
          )
 | 
						|
        : null;
 | 
						|
 | 
						|
    chatController.send(
 | 
						|
      // originalWritten: originalWritten,
 | 
						|
      originalSent: originalSent,
 | 
						|
      tokensSent: tokensSent,
 | 
						|
      //TODO - save originalwritten tokens
 | 
						|
      // choreo: applicableChoreo,
 | 
						|
      choreo: choreoRecord,
 | 
						|
    );
 | 
						|
 | 
						|
    clear();
 | 
						|
  }
 | 
						|
 | 
						|
  _resetDebounceTimer() {
 | 
						|
    if (debounceTimer != null) {
 | 
						|
      debounceTimer?.cancel();
 | 
						|
      debounceTimer = null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void onITStart(PangeaMatch itMatch) {
 | 
						|
    if (!itMatch.isITStart) {
 | 
						|
      throw Exception("this isn't an itStart match!");
 | 
						|
    }
 | 
						|
    choreoMode = ChoreoMode.it;
 | 
						|
    itController.initializeIT(
 | 
						|
      ITStartData(_textController.text, null),
 | 
						|
    );
 | 
						|
    itMatch.status = PangeaMatchStatus.accepted;
 | 
						|
 | 
						|
    choreoRecord.addRecord(_textController.text, match: itMatch);
 | 
						|
 | 
						|
    //PTODO - if totally in L1, save tokens, that's good stuff
 | 
						|
 | 
						|
    igc.clear();
 | 
						|
 | 
						|
    _textController.setSystemText("", EditType.itStart);
 | 
						|
  }
 | 
						|
 | 
						|
  /// Handles any changes to the text input
 | 
						|
  _onChangeListener() {
 | 
						|
    // Rebuild the IGC button if the state has changed.
 | 
						|
    // This accounts for user typing after initial IGC has completed
 | 
						|
    if (_currentAssistanceState != assistanceState) {
 | 
						|
      setState();
 | 
						|
    }
 | 
						|
 | 
						|
    if (_noChange) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (_textController.editType == EditType.igc ||
 | 
						|
        _textController.editType == EditType.itDismissed) {
 | 
						|
      _lastChecked = _textController.text;
 | 
						|
      _textController.editType = EditType.keyboard;
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // not sure if this is necessary now
 | 
						|
    MatrixState.pAnyState.closeOverlay();
 | 
						|
 | 
						|
    if (errorService.isError) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    igc.clear();
 | 
						|
 | 
						|
    _resetDebounceTimer();
 | 
						|
 | 
						|
    if (editTypeIsKeyboard) {
 | 
						|
      debounceTimer ??= Timer(
 | 
						|
        Duration(milliseconds: msBeforeIGCStart),
 | 
						|
        () => getLanguageHelp(),
 | 
						|
      );
 | 
						|
    } else {
 | 
						|
      getLanguageHelp();
 | 
						|
    }
 | 
						|
 | 
						|
    //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
 | 
						|
    //types when that action happens (e.g. an it/igc choice is selected)
 | 
						|
    textController.editType = EditType.keyboard;
 | 
						|
  }
 | 
						|
 | 
						|
  /// Fetches the language help for the current text, including grammar correction, language detection,
 | 
						|
  /// tokens, and translations. Includes logic to exit the flow if the user is not subscribed, if the tools are not enabled, or
 | 
						|
  /// or if autoIGC is not enabled and the user has not manually requested it.
 | 
						|
  /// [onlyTokensAndLanguageDetection] will
 | 
						|
  Future<void> getLanguageHelp({
 | 
						|
    bool manual = false,
 | 
						|
  }) async {
 | 
						|
    try {
 | 
						|
      if (errorService.isError) return;
 | 
						|
      final SubscriptionStatus canSendStatus =
 | 
						|
          pangeaController.subscriptionController.subscriptionStatus;
 | 
						|
 | 
						|
      if (canSendStatus != SubscriptionStatus.subscribed ||
 | 
						|
          l2Lang == null ||
 | 
						|
          l1Lang == null ||
 | 
						|
          (!igcEnabled && !itEnabled) ||
 | 
						|
          (!isAutoIGCEnabled && !manual && choreoMode != ChoreoMode.it)) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      startLoading();
 | 
						|
 | 
						|
      // if getting language assistance after finishing IT,
 | 
						|
      // reset the itController
 | 
						|
      if (choreoMode == ChoreoMode.it && itController.isTranslationDone) {
 | 
						|
        itController.clear();
 | 
						|
      }
 | 
						|
 | 
						|
      await (isRunningIT
 | 
						|
          ? itController.getTranslationData(_useCustomInput)
 | 
						|
          : igc.getIGCTextData());
 | 
						|
    } catch (err, stack) {
 | 
						|
      ErrorHandler.logError(
 | 
						|
        e: err,
 | 
						|
        s: stack,
 | 
						|
        data: {
 | 
						|
          "l2Lang": l2Lang?.toJson(),
 | 
						|
          "l1Lang": l1Lang?.toJson(),
 | 
						|
          "choreoMode": choreoMode,
 | 
						|
          "igcEnabled": igcEnabled,
 | 
						|
          "itEnabled": itEnabled,
 | 
						|
          "isAutoIGCEnabled": isAutoIGCEnabled,
 | 
						|
          "isTranslationDone": itController.isTranslationDone,
 | 
						|
          "useCustomInput": _useCustomInput,
 | 
						|
        },
 | 
						|
      );
 | 
						|
    } finally {
 | 
						|
      stopLoading();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void onITChoiceSelect(ITStep step) {
 | 
						|
    choreoRecord.addRecord(_textController.text, step: step);
 | 
						|
    _textController.setSystemText(
 | 
						|
      _textController.text + step.continuances[step.chosen!].text,
 | 
						|
      step.continuances[step.chosen!].gold
 | 
						|
          ? EditType.itGold
 | 
						|
          : EditType.itStandard,
 | 
						|
    );
 | 
						|
    _textController.selection =
 | 
						|
        TextSelection.collapsed(offset: _textController.text.length);
 | 
						|
    giveInputFocus();
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> onReplacementSelect({
 | 
						|
    required int matchIndex,
 | 
						|
    required int choiceIndex,
 | 
						|
  }) async {
 | 
						|
    try {
 | 
						|
      if (igc.igcTextData == null) {
 | 
						|
        ErrorHandler.logError(
 | 
						|
          e: "onReplacementSelect with null igcTextData",
 | 
						|
          s: StackTrace.current,
 | 
						|
          data: {
 | 
						|
            "matchIndex": matchIndex,
 | 
						|
            "choiceIndex": choiceIndex,
 | 
						|
          },
 | 
						|
        );
 | 
						|
        MatrixState.pAnyState.closeOverlay();
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      if (igc.igcTextData!.matches[matchIndex].match.choices == null) {
 | 
						|
        ErrorHandler.logError(
 | 
						|
          e: "onReplacementSelect with null choices",
 | 
						|
          s: StackTrace.current,
 | 
						|
          data: {
 | 
						|
            "igctextData": igc.igcTextData?.toJson(),
 | 
						|
            "matchIndex": matchIndex,
 | 
						|
            "choiceIndex": choiceIndex,
 | 
						|
          },
 | 
						|
        );
 | 
						|
        MatrixState.pAnyState.closeOverlay();
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      //if it's the wrong choice, return
 | 
						|
      // if (!igc.igcTextData!.matches[matchIndex].match.choices![choiceIndex]
 | 
						|
      //     .selected) {
 | 
						|
      //   igc.igcTextData!.matches[matchIndex].match.choices![choiceIndex]
 | 
						|
      //       .selected = true;
 | 
						|
      //   setState();
 | 
						|
      //   return;
 | 
						|
      // }
 | 
						|
 | 
						|
      igc.igcTextData!.matches[matchIndex].match.choices![choiceIndex]
 | 
						|
          .selected = true;
 | 
						|
 | 
						|
      final isNormalizationError =
 | 
						|
          igc.spanDataController.isNormalizationError(matchIndex);
 | 
						|
 | 
						|
      //if it's the right choice, replace in text
 | 
						|
      if (!isNormalizationError) {
 | 
						|
        choreoRecord.addRecord(
 | 
						|
          _textController.text,
 | 
						|
          match: igc.igcTextData!.matches[matchIndex].copyWith
 | 
						|
            ..status = PangeaMatchStatus.accepted,
 | 
						|
        );
 | 
						|
      }
 | 
						|
 | 
						|
      igc.igcTextData!.acceptReplacement(
 | 
						|
        matchIndex,
 | 
						|
        choiceIndex,
 | 
						|
      );
 | 
						|
 | 
						|
      _textController.setSystemText(
 | 
						|
        igc.igcTextData!.originalInput,
 | 
						|
        EditType.igc,
 | 
						|
      );
 | 
						|
 | 
						|
      MatrixState.pAnyState.closeOverlay();
 | 
						|
      setState();
 | 
						|
    } catch (err, stack) {
 | 
						|
      debugger(when: kDebugMode);
 | 
						|
      ErrorHandler.logError(
 | 
						|
        e: err,
 | 
						|
        s: stack,
 | 
						|
        data: {
 | 
						|
          "igctextData": igc.igcTextData?.toJson(),
 | 
						|
          "matchIndex": matchIndex,
 | 
						|
          "choiceIndex": choiceIndex,
 | 
						|
        },
 | 
						|
      );
 | 
						|
      igc.igcTextData?.matches.clear();
 | 
						|
    } finally {
 | 
						|
      setState();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void acceptNormalizationMatches() {
 | 
						|
    for (int i = 0; i < igc.igcTextData!.matches.length; i++) {
 | 
						|
      final isNormalizationError =
 | 
						|
          igc.spanDataController.isNormalizationError(i);
 | 
						|
 | 
						|
      if (!isNormalizationError) continue;
 | 
						|
      final match = igc.igcTextData!.matches[i];
 | 
						|
 | 
						|
      choreoRecord.addRecord(
 | 
						|
        _textController.text,
 | 
						|
        match: match.copyWith..status = PangeaMatchStatus.automatic,
 | 
						|
      );
 | 
						|
 | 
						|
      igc.igcTextData!.acceptReplacement(
 | 
						|
        i,
 | 
						|
        match.match.choices!.indexWhere(
 | 
						|
          (c) => c.isBestCorrection,
 | 
						|
        ),
 | 
						|
      );
 | 
						|
 | 
						|
      _textController.setSystemText(
 | 
						|
        igc.igcTextData!.originalInput,
 | 
						|
        EditType.igc,
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void onIgnoreMatch({required int cursorOffset}) {
 | 
						|
    try {
 | 
						|
      if (igc.igcTextData == null) {
 | 
						|
        debugger(when: kDebugMode);
 | 
						|
        ErrorHandler.logError(
 | 
						|
          m: "should not be in onIgnoreMatch with null igcTextData",
 | 
						|
          s: StackTrace.current,
 | 
						|
          data: {},
 | 
						|
        );
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      final int matchIndex = igc.igcTextData!.getTopMatchIndexForOffset(
 | 
						|
        cursorOffset,
 | 
						|
      );
 | 
						|
 | 
						|
      if (matchIndex == -1) {
 | 
						|
        debugger(when: kDebugMode);
 | 
						|
        throw Exception("Cannot find the ignored match in igcTextData");
 | 
						|
      }
 | 
						|
 | 
						|
      igc.onIgnoreMatch(igc.igcTextData!.matches[matchIndex]);
 | 
						|
      igc.igcTextData!.matches[matchIndex].status = PangeaMatchStatus.ignored;
 | 
						|
 | 
						|
      final isNormalizationError =
 | 
						|
          igc.spanDataController.isNormalizationError(matchIndex);
 | 
						|
 | 
						|
      if (!isNormalizationError) {
 | 
						|
        choreoRecord.addRecord(
 | 
						|
          _textController.text,
 | 
						|
          match: igc.igcTextData!.matches[matchIndex],
 | 
						|
        );
 | 
						|
      }
 | 
						|
 | 
						|
      igc.igcTextData!.matches.removeAt(matchIndex);
 | 
						|
    } catch (err, stack) {
 | 
						|
      debugger(when: kDebugMode);
 | 
						|
      Sentry.addBreadcrumb(
 | 
						|
        Breadcrumb(
 | 
						|
          data: {
 | 
						|
            "igcTextData": igc.igcTextData?.toJson(),
 | 
						|
            "offset": cursorOffset,
 | 
						|
          },
 | 
						|
        ),
 | 
						|
      );
 | 
						|
      ErrorHandler.logError(
 | 
						|
        e: err,
 | 
						|
        s: stack,
 | 
						|
        data: {
 | 
						|
          "igctextData": igc.igcTextData?.toJson(),
 | 
						|
        },
 | 
						|
      );
 | 
						|
      igc.igcTextData?.matches.clear();
 | 
						|
    } finally {
 | 
						|
      setState();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void giveInputFocus() {
 | 
						|
    Future.delayed(Duration.zero, () {
 | 
						|
      chatController.inputFocus.requestFocus();
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  String get currentText => _textController.text;
 | 
						|
 | 
						|
  PangeaTextController get textController => _textController;
 | 
						|
 | 
						|
  String get accessToken => pangeaController.userController.accessToken;
 | 
						|
 | 
						|
  clear() {
 | 
						|
    choreoMode = ChoreoMode.igc;
 | 
						|
    _lastChecked = null;
 | 
						|
    _timesClicked = 0;
 | 
						|
    isFetching = false;
 | 
						|
    choreoRecord = ChoreoRecord.newRecord;
 | 
						|
    itController.clear();
 | 
						|
    igc.dispose();
 | 
						|
    //@ggurdin - why is this commented out?
 | 
						|
    // errorService.clear();
 | 
						|
    _resetDebounceTimer();
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> onPaste(value) async {
 | 
						|
    choreoRecord.pastedStrings.add(value);
 | 
						|
  }
 | 
						|
 | 
						|
  dispose() {
 | 
						|
    _textController.dispose();
 | 
						|
    _trialStream?.cancel();
 | 
						|
    _languageStream?.cancel();
 | 
						|
    stateStream.close();
 | 
						|
    tts.dispose();
 | 
						|
  }
 | 
						|
 | 
						|
  LanguageModel? get l2Lang {
 | 
						|
    return pangeaController.languageController.activeL2Model();
 | 
						|
  }
 | 
						|
 | 
						|
  String? get l2LangCode => l2Lang?.langCode;
 | 
						|
 | 
						|
  LanguageModel? get l1Lang =>
 | 
						|
      pangeaController.languageController.activeL1Model();
 | 
						|
 | 
						|
  String? get l1LangCode => l1Lang?.langCode;
 | 
						|
 | 
						|
  String? get userId => pangeaController.userController.userId;
 | 
						|
 | 
						|
  bool get _noChange =>
 | 
						|
      _lastChecked != null && _lastChecked == _textController.text;
 | 
						|
 | 
						|
  bool get isRunningIT =>
 | 
						|
      choreoMode == ChoreoMode.it && !itController.isTranslationDone;
 | 
						|
 | 
						|
  void startLoading() {
 | 
						|
    _lastChecked = _textController.text;
 | 
						|
    isFetching = true;
 | 
						|
    setState();
 | 
						|
  }
 | 
						|
 | 
						|
  void stopLoading() {
 | 
						|
    isFetching = false;
 | 
						|
    setState();
 | 
						|
  }
 | 
						|
 | 
						|
  void incrementTimesClicked() {
 | 
						|
    if (assistanceState == AssistanceState.fetched) {
 | 
						|
      _timesClicked++;
 | 
						|
 | 
						|
      // if user is doing IT, call closeIT here to
 | 
						|
      // ensure source text is replaced when needed
 | 
						|
      if (itController.isOpen && _timesClicked > 1) {
 | 
						|
        itController.closeIT();
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  get roomId => chatController.roomId;
 | 
						|
 | 
						|
  bool get _useCustomInput => [
 | 
						|
        EditType.keyboard,
 | 
						|
        EditType.igc,
 | 
						|
        EditType.alternativeTranslation,
 | 
						|
      ].contains(_textController.editType);
 | 
						|
 | 
						|
  bool get editTypeIsKeyboard => EditType.keyboard == _textController.editType;
 | 
						|
 | 
						|
  setState() {
 | 
						|
    if (!stateStream.isClosed) {
 | 
						|
      stateStream.add(0);
 | 
						|
    }
 | 
						|
    _currentAssistanceState = assistanceState;
 | 
						|
  }
 | 
						|
 | 
						|
  LayerLinkAndKey get itBarLinkAndKey =>
 | 
						|
      MatrixState.pAnyState.layerLinkAndKey(itBarTransformTargetKey);
 | 
						|
 | 
						|
  String get itBarTransformTargetKey => 'it_bar$roomId';
 | 
						|
 | 
						|
  LayerLinkAndKey get inputLayerLinkAndKey =>
 | 
						|
      MatrixState.pAnyState.layerLinkAndKey(inputTransformTargetKey);
 | 
						|
 | 
						|
  String get inputTransformTargetKey => 'input$roomId';
 | 
						|
 | 
						|
  LayerLinkAndKey get itBotLayerLinkAndKey =>
 | 
						|
      MatrixState.pAnyState.layerLinkAndKey(itBotTransformTargetKey);
 | 
						|
 | 
						|
  String get itBotTransformTargetKey => 'itBot$roomId';
 | 
						|
 | 
						|
  bool get igcEnabled => pangeaController.permissionsController.isToolEnabled(
 | 
						|
        ToolSetting.interactiveGrammar,
 | 
						|
        chatController.room,
 | 
						|
      );
 | 
						|
 | 
						|
  bool get itEnabled => pangeaController.permissionsController.isToolEnabled(
 | 
						|
        ToolSetting.interactiveTranslator,
 | 
						|
        chatController.room,
 | 
						|
      );
 | 
						|
 | 
						|
  bool get immersionMode =>
 | 
						|
      pangeaController.permissionsController.isToolEnabled(
 | 
						|
        ToolSetting.immersionMode,
 | 
						|
        chatController.room,
 | 
						|
      );
 | 
						|
 | 
						|
  bool get isAutoIGCEnabled =>
 | 
						|
      pangeaController.permissionsController.isToolEnabled(
 | 
						|
        ToolSetting.autoIGC,
 | 
						|
        chatController.room,
 | 
						|
      );
 | 
						|
 | 
						|
  AssistanceState get assistanceState {
 | 
						|
    final isSubscribed = pangeaController.subscriptionController.isSubscribed;
 | 
						|
    if (isSubscribed != null && !isSubscribed) {
 | 
						|
      return AssistanceState.noSub;
 | 
						|
    }
 | 
						|
 | 
						|
    if (currentText.isEmpty && itController.sourceText == null) {
 | 
						|
      return AssistanceState.noMessage;
 | 
						|
    }
 | 
						|
 | 
						|
    if ((igc.igcTextData?.matches.isNotEmpty ?? false) || isRunningIT) {
 | 
						|
      return AssistanceState.fetched;
 | 
						|
    }
 | 
						|
 | 
						|
    if (isFetching) {
 | 
						|
      return AssistanceState.fetching;
 | 
						|
    }
 | 
						|
 | 
						|
    if (igc.igcTextData == null) {
 | 
						|
      return AssistanceState.notFetched;
 | 
						|
    }
 | 
						|
 | 
						|
    return AssistanceState.complete;
 | 
						|
  }
 | 
						|
 | 
						|
  bool get canSendMessage {
 | 
						|
    // if there's an error, let them send. we don't want to block them from sending in this case
 | 
						|
    if (errorService.isError ||
 | 
						|
        l2Lang == null ||
 | 
						|
        l1Lang == null ||
 | 
						|
        _timesClicked > 1) {
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
 | 
						|
    // if they're in IT mode, don't let them send
 | 
						|
    if (itEnabled && isRunningIT) return false;
 | 
						|
 | 
						|
    // if they've turned off IGC then let them send the message when they want
 | 
						|
    if (!isAutoIGCEnabled) return true;
 | 
						|
 | 
						|
    // if we're in the middle of fetching results, don't let them send
 | 
						|
    if (isFetching) return false;
 | 
						|
 | 
						|
    // they're supposed to run IGC but haven't yet, don't let them send
 | 
						|
    if (igc.igcTextData == null) {
 | 
						|
      return itController.dismissed;
 | 
						|
    }
 | 
						|
 | 
						|
    // if they have relevant matches, don't let them send
 | 
						|
    final hasITMatches =
 | 
						|
        igc.igcTextData!.matches.any((match) => match.isITStart);
 | 
						|
    final hasIGCMatches =
 | 
						|
        igc.igcTextData!.matches.any((match) => !match.isITStart);
 | 
						|
    if ((itEnabled && hasITMatches) || (igcEnabled && hasIGCMatches)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // otherwise, let them send
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
}
 |