From 08899b1598f456ae9d90984fc7eb9f40ee7161af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Poizat?= Date: Wed, 7 Jan 2026 17:33:41 +0100 Subject: [PATCH] Extract AI Scribe suggestion logic In Scribe desktop, the suggestion is displayed in a popup. In Scribe mobile, it will be displayed in a full screen modal. But it share the same logic about loading, success and error. So here I extract this logic and use it in the AI Scribe desktop to be able to use it later in AI Scribe mobile. --- scribe/lib/scribe.dart | 1 + .../ai_scribe_suggestion_state_mixin.dart | 112 ++++++++++++++++++ .../modal/ai_scribe_suggestion_widget.dart | 80 ++----------- 3 files changed, 125 insertions(+), 68 deletions(-) create mode 100644 scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart diff --git a/scribe/lib/scribe.dart b/scribe/lib/scribe.dart index 7591c2392..06ba7d826 100644 --- a/scribe/lib/scribe.dart +++ b/scribe/lib/scribe.dart @@ -25,6 +25,7 @@ export 'scribe/ai/presentation/utils/ai_scribe_constants.dart'; export 'scribe/ai/presentation/utils/context_menu/hover_submenu_controller.dart'; export 'scribe/ai/presentation/utils/context_menu/context_submenu_controller.dart'; export 'scribe/ai/presentation/utils/modal/ai_scribe_modal_manager.dart'; +export 'scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart'; export 'scribe/ai/presentation/utils/modal/anchored_modal_layout_calculator.dart'; export 'scribe/ai/presentation/widgets/button/ai_assistant_button.dart'; export 'scribe/ai/presentation/widgets/button/inline_ai_assist_button.dart'; diff --git a/scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart b/scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart new file mode 100644 index 000000000..441f9fcc1 --- /dev/null +++ b/scribe/lib/scribe/ai/presentation/utils/modal/ai_scribe_suggestion_state_mixin.dart @@ -0,0 +1,112 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart' as dartz; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:scribe/scribe.dart'; +import 'package:scribe/scribe/ai/data/network/ai_api_exception.dart'; + +mixin AiScribeSuggestionStateMixin on State { + GenerateAITextInteractor? get interactor => _interactor; + GenerateAITextInteractor? _interactor; + + ValueNotifier> get suggestionState => _suggestionState; + final ValueNotifier> _suggestionState = + ValueNotifier(dartz.Right(GenerateAITextLoading())); + + AIAction get aiAction; + String? get content; + ImagePaths get imagePaths; + OnSelectAiScribeSuggestionAction get onSelectAction; + + @override + void initState() { + super.initState(); + + if (!Get.isRegistered()) { + _suggestionState.value = dartz.Left( + GenerateAITextFailure( + GenerateAITextInteractorIsNotRegisteredException(), + ), + ); + return; + } + + _interactor = Get.find(); + loadSuggestion(); + } + + Future loadSuggestion() async { + _suggestionState.value = dartz.Right(GenerateAITextLoading()); + + final result = await _interactor!.execute( + aiAction, + content, + ); + + if (!mounted) return; + + result.fold( + (failure) => _suggestionState.value = dartz.Left(failure), + (success) => _suggestionState.value = dartz.Right(success), + ); + } + + Widget buildStateContent( + BuildContext context, + ScribeLocalizations localizations, + ) { + return ValueListenableBuilder>( + valueListenable: _suggestionState, + builder: (_, stateValue, __) { + return stateValue.fold( + (failure) => buildErrorState(localizations), + (value) { + if (value is GenerateAITextSuccess) { + final hasContent = content?.trim().isNotEmpty == true; + + return buildSuccessState( + value.response.result, + hasContent, + localizations, + ); + } + return buildLoadingState(localizations); + }, + ); + }, + ); + } + + Widget buildLoadingState(ScribeLocalizations localizations) { + return AiScribeSuggestionLoading( + imagePaths: imagePaths, + ); + } + + Widget buildErrorState(ScribeLocalizations localizations) { + return AiScribeSuggestionError( + imagePaths: imagePaths, + ); + } + + Widget buildSuccessState( + String suggestionText, + bool hasContent, + ScribeLocalizations localizations, + ) { + return AiScribeSuggestionSuccess( + imagePaths: imagePaths, + suggestionText: suggestionText, + hasContent: hasContent, + onSelectAction: onSelectAction, + ); + } + + @override + void dispose() { + _suggestionState.dispose(); + super.dispose(); + } +} diff --git a/scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart b/scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart index 7e2d753ff..279009a10 100644 --- a/scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart +++ b/scribe/lib/scribe/ai/presentation/widgets/modal/ai_scribe_suggestion_widget.dart @@ -1,14 +1,9 @@ import 'dart:math'; import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/presentation/state/failure.dart'; -import 'package:core/presentation/state/success.dart'; -import 'package:dartz/dartz.dart' as dartz; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:scribe/scribe.dart'; -import 'package:scribe/scribe/ai/data/network/ai_api_exception.dart'; class AiScribeSuggestionWidget extends StatefulWidget { final AIAction aiAction; @@ -37,42 +32,20 @@ class AiScribeSuggestionWidget extends StatefulWidget { _AiScribeSuggestionWidgetState(); } -class _AiScribeSuggestionWidgetState extends State { - GenerateAITextInteractor? _interactor; - - final ValueNotifier> _state = - ValueNotifier(dartz.Right(GenerateAITextLoading())); +class _AiScribeSuggestionWidgetState extends State + with AiScribeSuggestionStateMixin { + @override + AIAction get aiAction => widget.aiAction; @override - void initState() { - super.initState(); + String? get content => widget.content; - if (!Get.isRegistered()) { - _state.value = dartz.Left( - GenerateAITextFailure( - const GenerateAITextInteractorIsNotRegisteredException(), - ), - ); - return; - } + @override + ImagePaths get imagePaths => widget.imagePaths; - _interactor = Get.find(); - _loadSuggestion(); - } - - Future _loadSuggestion() async { - final result = await _interactor!.execute( - widget.aiAction, - widget.content, - ); - - if (!mounted) return; - - result.fold( - (failure) => _state.value = dartz.Left(failure), - (success) => _state.value = dartz.Right(success), - ); - } + @override + OnSelectAiScribeSuggestionAction get onSelectAction => + widget.onSelectAiScribeSuggestionAction; @override Widget build(BuildContext context) { @@ -147,30 +120,7 @@ class _AiScribeSuggestionWidgetState extends State { imagePaths: widget.imagePaths, ), Flexible( - child: ValueListenableBuilder>( - valueListenable: _state, - builder: (_, state, __) { - return state.fold( - (_) => AiScribeSuggestionError( - imagePaths: widget.imagePaths, - ), - (value) { - if (value is GenerateAITextSuccess) { - return AiScribeSuggestionSuccess( - imagePaths: widget.imagePaths, - suggestionText: value.response.result, - hasContent: hasContent, - onSelectAction: widget.onSelectAiScribeSuggestionAction, - ); - } - - return AiScribeSuggestionLoading( - imagePaths: widget.imagePaths, - ); - }, - ); - }, - ), + child: buildStateContent(context, localizations), ), ], ); @@ -205,7 +155,7 @@ class _AiScribeSuggestionWidgetState extends State { widget.buttonPosition != null && widget.buttonSize != null; void _handleClickOutside() { - final shouldDismiss = _state.value.fold( + final shouldDismiss = suggestionState.value.fold( (failure) => failure is GenerateAITextFailure, (success) => success is GenerateAITextSuccess, ); @@ -213,10 +163,4 @@ class _AiScribeSuggestionWidgetState extends State { Navigator.of(context).pop(); } } - - @override - void dispose() { - _state.dispose(); - super.dispose(); - } }