feat(ai-scribe): Avoid the prompt being obscured when there is a lot of content.
This commit is contained in:
@@ -16,6 +16,7 @@ export 'scribe/ai/presentation/model/context_menu/ai_scribe_context_menu_action.
|
|||||||
export 'scribe/ai/presentation/model/context_menu/ai_scribe_suggestion_actions.dart';
|
export 'scribe/ai/presentation/model/context_menu/ai_scribe_suggestion_actions.dart';
|
||||||
export 'scribe/ai/presentation/model/modal/anchored_modal_layout_input.dart';
|
export 'scribe/ai/presentation/model/modal/anchored_modal_layout_input.dart';
|
||||||
export 'scribe/ai/presentation/model/modal/anchored_modal_layout_result.dart';
|
export 'scribe/ai/presentation/model/modal/anchored_modal_layout_result.dart';
|
||||||
|
export 'scribe/ai/presentation/model/modal/anchored_suggestion_layout_result.dart';
|
||||||
export 'scribe/ai/presentation/model/modal/modal_cross_axis_alignment.dart';
|
export 'scribe/ai/presentation/model/modal/modal_cross_axis_alignment.dart';
|
||||||
export 'scribe/ai/presentation/model/modal/modal_placement.dart';
|
export 'scribe/ai/presentation/model/modal/modal_placement.dart';
|
||||||
export 'scribe/ai/presentation/model/text_selection_model.dart';
|
export 'scribe/ai/presentation/model/text_selection_model.dart';
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
class AnchoredSuggestionLayoutResult {
|
||||||
|
final Offset offset;
|
||||||
|
final double availableHeight;
|
||||||
|
final double? top;
|
||||||
|
final double? bottom;
|
||||||
|
|
||||||
|
const AnchoredSuggestionLayoutResult({
|
||||||
|
required this.offset,
|
||||||
|
required this.availableHeight,
|
||||||
|
this.top,
|
||||||
|
this.bottom,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/animation.dart';
|
import 'package:flutter/animation.dart';
|
||||||
import 'package:scribe/scribe.dart';
|
import 'package:scribe/scribe.dart';
|
||||||
|
|
||||||
@@ -138,4 +140,42 @@ class AnchoredModalLayoutCalculator {
|
|||||||
return anchorStart + anchorSize - menuSize;
|
return anchorStart + anchorSize - menuSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static AnchoredSuggestionLayoutResult calculateAnchoredSuggestLayout({
|
||||||
|
required Size screenSize,
|
||||||
|
required Offset anchorPosition,
|
||||||
|
required Size anchorSize,
|
||||||
|
required double modalMaxHeight,
|
||||||
|
ModalPlacement? preferredPlacement,
|
||||||
|
double gap = 8,
|
||||||
|
double padding = AIScribeSizes.screenEdgePadding,
|
||||||
|
}) {
|
||||||
|
final isTop = preferredPlacement == ModalPlacement.top;
|
||||||
|
|
||||||
|
if (isTop) {
|
||||||
|
final availableHeight = anchorPosition.dy - padding - gap;
|
||||||
|
final positionBottom = screenSize.height - anchorPosition.dy + gap;
|
||||||
|
|
||||||
|
return AnchoredSuggestionLayoutResult(
|
||||||
|
offset: Offset(
|
||||||
|
anchorPosition.dx,
|
||||||
|
positionBottom,
|
||||||
|
),
|
||||||
|
availableHeight: availableHeight,
|
||||||
|
bottom: positionBottom,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final spaceBelow = screenSize.height - anchorPosition.dy - padding - gap;
|
||||||
|
final positionTop = anchorPosition.dx + anchorSize.width + gap;
|
||||||
|
|
||||||
|
return AnchoredSuggestionLayoutResult(
|
||||||
|
offset: Offset(
|
||||||
|
positionTop,
|
||||||
|
anchorPosition.dy,
|
||||||
|
),
|
||||||
|
availableHeight: min(spaceBelow, modalMaxHeight),
|
||||||
|
top: anchorPosition.dy,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-109
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:core/presentation/resources/image_paths.dart';
|
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:dartz/dartz.dart' as dartz;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -32,147 +34,164 @@ class AiScribeSuggestionWidget extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
State<AiScribeSuggestionWidget> createState() =>
|
State<AiScribeSuggestionWidget> createState() =>
|
||||||
_AiScribeSuggestionWidgetModalState();
|
_AiScribeSuggestionWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AiScribeSuggestionWidgetModalState
|
class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget> {
|
||||||
extends State<AiScribeSuggestionWidget> {
|
GenerateAITextInteractor? _interactor;
|
||||||
GenerateAITextInteractor? _generateAITextInteractor;
|
|
||||||
final ValueNotifier<dynamic> _valueNotifier =
|
final ValueNotifier<dartz.Either<Failure, Success>> _state =
|
||||||
ValueNotifier(dartz.Right(GenerateAITextLoading()));
|
ValueNotifier(dartz.Right(GenerateAITextLoading()));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (Get.isRegistered<GenerateAITextInteractor>()) {
|
|
||||||
_generateAITextInteractor = Get.find<GenerateAITextInteractor>();
|
if (!Get.isRegistered<GenerateAITextInteractor>()) {
|
||||||
_getAiSuggestion(
|
_state.value = dartz.Left(
|
||||||
_generateAITextInteractor!,
|
|
||||||
widget.aiAction,
|
|
||||||
widget.content,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_valueNotifier.value = dartz.Left(
|
|
||||||
GenerateAITextFailure(
|
GenerateAITextFailure(
|
||||||
GenerateAITextInteractorIsNotRegisteredException(),
|
GenerateAITextInteractorIsNotRegisteredException(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_interactor = Get.find<GenerateAITextInteractor>();
|
||||||
|
_loadSuggestion();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _getAiSuggestion(
|
Future<void> _loadSuggestion() async {
|
||||||
GenerateAITextInteractor interactor,
|
final result = await _interactor!.execute(
|
||||||
AIAction action,
|
widget.aiAction,
|
||||||
String? content,
|
widget.content,
|
||||||
) async {
|
);
|
||||||
final result = await interactor.execute(action, content);
|
|
||||||
|
|
||||||
result.fold((failure) {
|
result.fold(
|
||||||
_valueNotifier.value = failure;
|
(failure) => _state.value = dartz.Left(failure),
|
||||||
}, (success) {
|
(success) => _state.value = dartz.Right(success),
|
||||||
_valueNotifier.value = success;
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final localizations = ScribeLocalizations.of(context);
|
|
||||||
final screenSize = MediaQuery.sizeOf(context);
|
final screenSize = MediaQuery.sizeOf(context);
|
||||||
|
|
||||||
final modalWidth = min(
|
final modalWidth = min(
|
||||||
screenSize.width * AIScribeSizes.mobileFactor,
|
screenSize.width * AIScribeSizes.mobileFactor,
|
||||||
AIScribeSizes.suggestionModalMaxWidth,
|
AIScribeSizes.suggestionModalMaxWidth,
|
||||||
);
|
);
|
||||||
final modalHeight = min(
|
|
||||||
|
final modalMaxHeight = min(
|
||||||
screenSize.height * AIScribeSizes.mobileFactor,
|
screenSize.height * AIScribeSizes.mobileFactor,
|
||||||
AIScribeSizes.suggestionModalMaxHeight,
|
AIScribeSizes.suggestionModalMaxHeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
final dialogContent = Container(
|
final dialogContent = _buildDialogContent(context);
|
||||||
width: modalWidth,
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
minHeight: AIScribeSizes.suggestionModalMinHeight,
|
|
||||||
maxHeight: modalHeight,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AIScribeColors.background,
|
|
||||||
borderRadius: const BorderRadius.all(
|
|
||||||
Radius.circular(AIScribeSizes.menuRadius),
|
|
||||||
),
|
|
||||||
boxShadow: AIScribeShadows.modal,
|
|
||||||
),
|
|
||||||
padding: AIScribeSizes.suggestionContentPadding,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
spacing: 8,
|
|
||||||
children: [
|
|
||||||
AiScribeSuggestionHeader(
|
|
||||||
title: widget.aiAction.getLabel(localizations),
|
|
||||||
imagePaths: widget.imagePaths,
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: ValueListenableBuilder(
|
|
||||||
valueListenable: _valueNotifier,
|
|
||||||
builder: (_, value, __) {
|
|
||||||
if (value is GenerateAITextSuccess) {
|
|
||||||
return AiScribeSuggestionSuccess(
|
|
||||||
imagePaths: widget.imagePaths,
|
|
||||||
suggestionText: value.response.result,
|
|
||||||
onSelectAction: widget.onSelectAiScribeSuggestionAction,
|
|
||||||
);
|
|
||||||
} else if (value is GenerateAITextFailure) {
|
|
||||||
return AiScribeSuggestionError(imagePaths: widget.imagePaths);
|
|
||||||
} else {
|
|
||||||
return AiScribeSuggestionLoading(
|
|
||||||
imagePaths: widget.imagePaths,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (widget.buttonPosition != null && widget.buttonSize != null) {
|
if (!_hasAnchor) {
|
||||||
final layoutResult = AnchoredModalLayoutCalculator.calculate(
|
return Center(
|
||||||
input: AnchoredModalLayoutInput(
|
child: _buildModalContainer(
|
||||||
screenSize: MediaQuery.of(context).size,
|
width: modalWidth,
|
||||||
anchorPosition: widget.buttonPosition!,
|
maxHeight: modalMaxHeight,
|
||||||
anchorSize: widget.buttonSize!,
|
child: dialogContent,
|
||||||
menuSize: Size(
|
|
||||||
modalWidth,
|
|
||||||
AIScribeSizes.suggestionModalMinHeight,
|
|
||||||
),
|
|
||||||
preferredPlacement: widget.preferredPlacement,
|
|
||||||
crossAxisAlignment: widget.crossAxisAlignment,
|
|
||||||
),
|
|
||||||
padding: AIScribeSizes.screenEdgePadding,
|
|
||||||
);
|
|
||||||
|
|
||||||
final position = layoutResult.position;
|
|
||||||
|
|
||||||
final top = widget.preferredPlacement == ModalPlacement.top
|
|
||||||
? position.dy - AIScribeSizes.modalWithoutContentSpacing
|
|
||||||
: position.dy;
|
|
||||||
|
|
||||||
return PointerInterceptor(
|
|
||||||
child: GestureDetector(
|
|
||||||
behavior: HitTestBehavior.opaque,
|
|
||||||
onTap: Navigator.of(context).pop,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
PositionedDirectional(
|
|
||||||
start: position.dx,
|
|
||||||
top: top,
|
|
||||||
child: dialogContent,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Center(child: dialogContent);
|
final layout = AnchoredModalLayoutCalculator.calculateAnchoredSuggestLayout(
|
||||||
|
screenSize: screenSize,
|
||||||
|
anchorPosition: widget.buttonPosition!,
|
||||||
|
anchorSize: widget.buttonSize!,
|
||||||
|
modalMaxHeight: modalMaxHeight,
|
||||||
|
preferredPlacement: widget.preferredPlacement,
|
||||||
|
);
|
||||||
|
|
||||||
|
return PointerInterceptor(
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: Navigator.of(context).pop,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
PositionedDirectional(
|
||||||
|
start: layout.offset.dx,
|
||||||
|
top: layout.top,
|
||||||
|
bottom: layout.bottom,
|
||||||
|
child: _buildModalContainer(
|
||||||
|
width: modalWidth,
|
||||||
|
maxHeight: layout.availableHeight,
|
||||||
|
child: dialogContent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildDialogContent(BuildContext context) {
|
||||||
|
final localizations = ScribeLocalizations.of(context);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
AiScribeSuggestionHeader(
|
||||||
|
title: widget.aiAction.getLabel(localizations),
|
||||||
|
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,
|
||||||
|
onSelectAction: widget.onSelectAiScribeSuggestionAction,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AiScribeSuggestionLoading(
|
||||||
|
imagePaths: widget.imagePaths,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildModalContainer({
|
||||||
|
required double width,
|
||||||
|
required double maxHeight,
|
||||||
|
required Widget child,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
width: width,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: AIScribeSizes.suggestionModalMinHeight,
|
||||||
|
maxHeight: maxHeight,
|
||||||
|
),
|
||||||
|
padding: AIScribeSizes.suggestionContentPadding,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AIScribeColors.background,
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
AIScribeSizes.menuRadius,
|
||||||
|
),
|
||||||
|
boxShadow: AIScribeShadows.modal,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _hasAnchor =>
|
||||||
|
widget.buttonPosition != null && widget.buttonSize != null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user