Add methods to display AI scribe
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||
import 'package:core/presentation/resources/image_paths.dart';
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:scribe/scribe/ai/data/datasource_impl/ai_datasource_impl.dart';
|
||||
import 'package:scribe/scribe/ai/data/repository/ai_repository_impl.dart';
|
||||
import 'package:scribe/scribe/ai/domain/state/generate_ai_text_state.dart';
|
||||
import 'package:scribe/scribe/ai/domain/usecases/generate_ai_text_interactor.dart';
|
||||
import 'package:scribe/scribe/ai/presentation/model/ai_action.dart';
|
||||
import 'package:scribe/scribe/ai/presentation/model/ai_scribe_menu_action.dart';
|
||||
import 'package:scribe/scribe/ai/presentation/styles/ai_scribe_styles.dart';
|
||||
import 'package:scribe/scribe/ai/presentation/widgets/ai_scribe_bar.dart';
|
||||
import 'package:scribe/scribe/ai/presentation/widgets/ai_scribe_menu.dart';
|
||||
import 'package:scribe/scribe/ai/presentation/widgets/ai_scribe_suggestion.dart';
|
||||
|
||||
typedef AIScribeResultCallback = void Function(String result);
|
||||
|
||||
Future<void> showAIScribeDialog({
|
||||
required BuildContext context,
|
||||
required ImagePaths imagePaths,
|
||||
required String content,
|
||||
required AIScribeResultCallback onInsertText,
|
||||
List<AIScribeMenuCategory>? availableCategories,
|
||||
Offset? buttonPosition,
|
||||
}) async {
|
||||
final hasContent = content.isNotEmpty;
|
||||
|
||||
final selectedAction = await showDialog<AIAction>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
final dialogContent = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (hasContent) ...[
|
||||
Material(
|
||||
color: Colors.white,
|
||||
elevation: AIScribeSizes.dialogElevation,
|
||||
borderRadius: BorderRadius.circular(AIScribeSizes.menuBorderRadius),
|
||||
child: SizedBox(
|
||||
width: AIScribeSizes.menuWidth,
|
||||
child: AIScribeMenu(
|
||||
onActionSelected: (action) {
|
||||
Navigator.of(context).pop(PredefinedAction(action));
|
||||
},
|
||||
availableCategories: availableCategories,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AIScribeSizes.fieldSpacing),
|
||||
],
|
||||
Material(
|
||||
color: Colors.white,
|
||||
elevation: AIScribeSizes.dialogElevation,
|
||||
borderRadius: BorderRadius.circular(AIScribeSizes.menuBorderRadius),
|
||||
child: SizedBox(
|
||||
width: AIScribeSizes.barWidth,
|
||||
child: AIScribeBar(
|
||||
onCustomPrompt: (customPrompt) {
|
||||
Navigator.of(context).pop(CustomPromptAction(customPrompt));
|
||||
},
|
||||
imagePaths: imagePaths
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (buttonPosition != null) {
|
||||
final position = _calculateModalPosition(
|
||||
context: context,
|
||||
buttonPosition: buttonPosition,
|
||||
modalWidth: AIScribeSizes.barWidth,
|
||||
);
|
||||
|
||||
return PointerInterceptor(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: position.left,
|
||||
bottom: position.bottom,
|
||||
child: dialogContent,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Center(child: dialogContent);
|
||||
},
|
||||
);
|
||||
|
||||
if (selectedAction == null) {
|
||||
log('showAIScribeDialog: No action selected');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (context) {
|
||||
final suggestionFuture = _executeAIRequest(selectedAction, content);
|
||||
final title = selectedAction.getLabel(context);
|
||||
|
||||
final modalContent = AIScribeSuggestion(
|
||||
title: title,
|
||||
suggestionFuture: suggestionFuture,
|
||||
onClose: () => Navigator.of(context).pop(),
|
||||
onInsert: (result) {
|
||||
onInsertText(result);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
imagePaths: imagePaths,
|
||||
);
|
||||
|
||||
if (buttonPosition != null) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final modalWidth = screenSize.width < AIScribeSizes.mobileBreakpoint
|
||||
? screenSize.width * AIScribeSizes.mobileWidthPercentage
|
||||
: AIScribeSizes.modalMaxWidthLargeScreen;
|
||||
|
||||
final position = _calculateModalPosition(
|
||||
context: context,
|
||||
buttonPosition: buttonPosition,
|
||||
modalWidth: modalWidth,
|
||||
modalHeight: AIScribeSizes.modalMaxHeight,
|
||||
);
|
||||
|
||||
return PointerInterceptor(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: position.left,
|
||||
bottom: position.bottom,
|
||||
child: modalContent,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Center(child: modalContent);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
({double left, double bottom}) _calculateModalPosition({
|
||||
required BuildContext context,
|
||||
required Offset buttonPosition,
|
||||
required double modalWidth,
|
||||
double? modalHeight,
|
||||
}) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
|
||||
// Calculate position above the button
|
||||
double left = buttonPosition.dx;
|
||||
double bottom = screenSize.height - buttonPosition.dy + AIScribeSizes.fieldSpacing;
|
||||
|
||||
// Ensure modal doesn't go off screen horizontally
|
||||
if (left + modalWidth > screenSize.width) {
|
||||
left = screenSize.width - modalWidth - AIScribeSizes.screenEdgePadding;
|
||||
}
|
||||
if (left < AIScribeSizes.screenEdgePadding) {
|
||||
left = AIScribeSizes.screenEdgePadding;
|
||||
}
|
||||
|
||||
// Ensure modal doesn't go off screen vertically (if height is provided)
|
||||
if (modalHeight != null) {
|
||||
if (bottom + modalHeight > screenSize.height) {
|
||||
bottom = screenSize.height - modalHeight - AIScribeSizes.screenEdgePadding;
|
||||
}
|
||||
if (bottom < AIScribeSizes.screenEdgePadding) {
|
||||
bottom = AIScribeSizes.screenEdgePadding;
|
||||
}
|
||||
}
|
||||
|
||||
return (left: left, bottom: bottom);
|
||||
}
|
||||
|
||||
Future<String> _executeAIRequest(
|
||||
AIAction action,
|
||||
String content,
|
||||
) async {
|
||||
final dio = Dio();
|
||||
final dataSource = AIDataSourceImpl(dio: dio);
|
||||
final repository = AIScribeRepositoryImpl(dataSource);
|
||||
final interactor = GenerateAITextInteractor(repository);
|
||||
|
||||
final result = await interactor.execute(action, content);
|
||||
|
||||
return result.fold(
|
||||
(failure) {
|
||||
throw Exception('Failed to generate AI response: $failure');
|
||||
},
|
||||
(success) {
|
||||
if (success is GenerateAITextSuccess) {
|
||||
return success.response.result;
|
||||
} else {
|
||||
throw Exception('Unexpected success state: $success');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user