diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index ef3042700..a7fb07449 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3951,7 +3951,7 @@ "autoIGCToolName": "Run Language Assistance Automatically", "autoIGCToolDescription": "Automatically run language assistance after typing messages", "runGrammarCorrection": "Run grammar correction", - "grammarCorrectionFailed": "Grammar correction failed", + "grammarCorrectionFailed": "Issues to address", "grammarCorrectionComplete": "Grammar correction complete", "leaveRoomDescription": "The chat will be moved to the archive. Other users will be able to see that you have left the chat.", "archiveSpaceDescription": "All chats within this space will be moved to the archive for yourself and other non-admin users.", diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 646543f22..b0c8dd462 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -19,14 +19,37 @@ import '../../repo/tokens_repo.dart'; import '../../utils/error_handler.dart'; import '../../utils/overlay.dart'; +class _SpanDetailsCacheItem { + SpanDetailsRepoReqAndRes data; + + _SpanDetailsCacheItem({required this.data}); +} + class IgcController { Choreographer choreographer; IGCTextData? igcTextData; Object? igcError; Completer igcCompleter = Completer(); + final Map _cache = {}; + Timer? _cacheClearTimer; + + IgcController(this.choreographer) { + _initializeCacheClearing(); + } + + void _initializeCacheClearing() { + const duration = Duration(minutes: 2); + _cacheClearTimer = Timer.periodic(duration, (Timer t) => _clearCache()); + } + + void _clearCache() { + _cache.clear(); + } - IgcController(this.choreographer); + void dispose() { + _cacheClearTimer?.cancel(); + } Future getIGCTextData({required bool tokensOnly}) async { try { @@ -80,6 +103,14 @@ class IgcController { igcTextData = igcTextDataResponse; + // After fetching igc data, pre-call span details for each match optimistically. + // This will make the loading of span details faster for the user + if (igcTextData?.matches.isNotEmpty ?? false) { + for (int i = 0; i < igcTextData!.matches.length; i++) { + getSpanDetails(i); + } + } + debugPrint("igc text ${igcTextData.toString()}"); } catch (err, stack) { debugger(when: kDebugMode); @@ -99,18 +130,38 @@ class IgcController { debugger(when: kDebugMode); return; } - final SpanData span = igcTextData!.matches[matchIndex].match; - final SpanDetailsRepoReqAndRes response = await SpanDataRepo.getSpanDetails( - await choreographer.accessToken, - request: SpanDetailsRepoReqAndRes( - userL1: choreographer.l1LangCode!, - userL2: choreographer.l2LangCode!, - enableIGC: choreographer.igcEnabled, - enableIT: choreographer.itEnabled, - span: span, - ), + /// Retrieves the span data from the `igcTextData` matches at the specified `matchIndex`. + /// Creates a `SpanDetailsRepoReqAndRes` object with the retrieved span data and other parameters. + /// Generates a cache key based on the created `SpanDetailsRepoReqAndRes` object. + final SpanData span = igcTextData!.matches[matchIndex].match; + final req = SpanDetailsRepoReqAndRes( + userL1: choreographer.l1LangCode!, + userL2: choreographer.l2LangCode!, + enableIGC: choreographer.igcEnabled, + enableIT: choreographer.itEnabled, + span: span, ); + final int cacheKey = req.hashCode; + + /// Retrieves the [SpanDetailsRepoReqAndRes] response from the cache if it exists, + /// otherwise makes an API call to get the response and stores it in the cache. + SpanDetailsRepoReqAndRes response; + if (_cache.containsKey(cacheKey)) { + response = _cache[cacheKey]!.data; + } else { + response = await SpanDataRepo.getSpanDetails( + await choreographer.accessToken, + request: SpanDetailsRepoReqAndRes( + userL1: choreographer.l1LangCode!, + userL2: choreographer.l2LangCode!, + enableIGC: choreographer.igcEnabled, + enableIT: choreographer.itEnabled, + span: span, + ), + ); + _cache[cacheKey] = _SpanDetailsCacheItem(data: response); + } try { igcTextData!.matches[matchIndex].match = response.span; diff --git a/lib/pangea/choreographer/widgets/start_igc_button.dart b/lib/pangea/choreographer/widgets/start_igc_button.dart index 183ac690d..5f786306f 100644 --- a/lib/pangea/choreographer/widgets/start_igc_button.dart +++ b/lib/pangea/choreographer/widgets/start_igc_button.dart @@ -33,7 +33,7 @@ class StartIGCButtonState extends State void initState() { _controller = AnimationController( vsync: this, - duration: const Duration(seconds: 1), + duration: const Duration(seconds: 2), ); choreoListener = widget.controller.choreographer.stateListener.stream .listen(updateSpinnerState); @@ -54,14 +54,15 @@ class StartIGCButtonState extends State @override Widget build(BuildContext context) { - if (widget.controller.choreographer.isAutoIGCEnabled) { + if (widget.controller.choreographer.isAutoIGCEnabled || + widget.controller.choreographer.choreoMode == ChoreoMode.it) { return const SizedBox.shrink(); } final Widget icon = Icon( Icons.autorenew_rounded, size: 46, - color: assistanceState.stateColor, + color: assistanceState.stateColor(context), ); return SizedBox( @@ -71,15 +72,23 @@ class StartIGCButtonState extends State tooltip: assistanceState.tooltip( L10n.of(context)!, ), - backgroundColor: Colors.white, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, disabledElevation: 0, shape: const CircleBorder(), onPressed: () { if (assistanceState != AssistanceState.complete) { - widget.controller.choreographer.getLanguageHelp( + widget.controller.choreographer + .getLanguageHelp( false, true, - ); + ) + .then((_) { + if (widget.controller.choreographer.igc.igcTextData != null && + widget.controller.choreographer.igc.igcTextData!.matches + .isNotEmpty) { + widget.controller.choreographer.igc.showFirstMatch(context); + } + }); } }, child: Stack( @@ -95,9 +104,9 @@ class StartIGCButtonState extends State Container( width: 26, height: 26, - decoration: const BoxDecoration( + decoration: BoxDecoration( shape: BoxShape.circle, - color: Colors.white, + color: Theme.of(context).scaffoldBackgroundColor, ), ), Container( @@ -105,13 +114,13 @@ class StartIGCButtonState extends State height: 20, decoration: BoxDecoration( shape: BoxShape.circle, - color: assistanceState.stateColor, + color: assistanceState.stateColor(context), ), ), - const Icon( + Icon( size: 16, Icons.check, - color: Colors.white, + color: Theme.of(context).scaffoldBackgroundColor, ), ], ), @@ -121,12 +130,12 @@ class StartIGCButtonState extends State } extension AssistanceStateExtension on AssistanceState { - Color get stateColor { + Color stateColor(context) { switch (this) { case AssistanceState.noMessage: case AssistanceState.notFetched: case AssistanceState.fetching: - return AppConfig.primaryColor; + return Theme.of(context).colorScheme.primary; case AssistanceState.fetched: return PangeaColors.igcError; case AssistanceState.complete: diff --git a/lib/pangea/repo/span_data_repo.dart b/lib/pangea/repo/span_data_repo.dart index c6025af6c..e253bb1d0 100644 --- a/lib/pangea/repo/span_data_repo.dart +++ b/lib/pangea/repo/span_data_repo.dart @@ -72,6 +72,24 @@ class SpanDetailsRepoReqAndRes { enableIGC: json['enable_igc'] as bool, span: SpanData.fromJson(json['span']), ); + + /// Overrides the equality operator to compare two [SpanDetailsRepoReqAndRes] objects. + /// Returns true if the objects are identical or have the same property + /// values (based on the results of the toJson function), false otherwise. + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! SpanDetailsRepoReqAndRes) return false; + + return toJson().toString() == other.toJson().toString(); + } + + /// Overrides the hashCode getter to generate a hash code for the [SpanDetailsRepoReqAndRes] object. + /// Used as keys in response cache in igc_controller. + @override + int get hashCode { + return toJson().toString().hashCode; + } } final spanDataRepomockSpan = SpanData(