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.
This commit is contained in:
Théo Poizat
2026-01-07 17:33:41 +01:00
committed by Dat H. Pham
parent 16b1de1b48
commit 08899b1598
3 changed files with 125 additions and 68 deletions
+1
View File
@@ -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';
@@ -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<T extends StatefulWidget> on State<T> {
GenerateAITextInteractor? get interactor => _interactor;
GenerateAITextInteractor? _interactor;
ValueNotifier<dartz.Either<Failure, Success>> get suggestionState => _suggestionState;
final ValueNotifier<dartz.Either<Failure, Success>> _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<GenerateAITextInteractor>()) {
_suggestionState.value = dartz.Left(
GenerateAITextFailure(
GenerateAITextInteractorIsNotRegisteredException(),
),
);
return;
}
_interactor = Get.find<GenerateAITextInteractor>();
loadSuggestion();
}
Future<void> 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<dartz.Either<Failure, Success>>(
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();
}
}
@@ -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<AiScribeSuggestionWidget> {
GenerateAITextInteractor? _interactor;
final ValueNotifier<dartz.Either<Failure, Success>> _state =
ValueNotifier(dartz.Right(GenerateAITextLoading()));
class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
with AiScribeSuggestionStateMixin {
@override
AIAction get aiAction => widget.aiAction;
@override
void initState() {
super.initState();
String? get content => widget.content;
if (!Get.isRegistered<GenerateAITextInteractor>()) {
_state.value = dartz.Left(
GenerateAITextFailure(
const GenerateAITextInteractorIsNotRegisteredException(),
),
);
return;
}
@override
ImagePaths get imagePaths => widget.imagePaths;
_interactor = Get.find<GenerateAITextInteractor>();
_loadSuggestion();
}
Future<void> _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<AiScribeSuggestionWidget> {
imagePaths: widget.imagePaths,
),
Flexible(
child: ValueListenableBuilder<dartz.Either<Failure, Success>>(
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<AiScribeSuggestionWidget> {
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<AiScribeSuggestionWidget> {
Navigator.of(context).pop();
}
}
@override
void dispose() {
_state.dispose();
super.dispose();
}
}