TF-4378 Migrate scribe-mobile branch to master branch
This commit is contained in:
@@ -258,8 +258,8 @@ class StringConvert {
|
||||
}
|
||||
|
||||
static String convertTextContentToHtmlContent(String textContent) {
|
||||
final htmlContent = textContent.replaceAll('\n', '<br>');
|
||||
|
||||
final escapedText = escapeTextContent(textContent);
|
||||
final htmlContent = escapedText.replaceAll('\n', '<br>');
|
||||
return '<div>$htmlContent</div>';
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -23,7 +23,10 @@ class RichTextMobileTabletController extends GetxController {
|
||||
try {
|
||||
await htmlEditorApi?.webViewController.evaluateJavascript(source: '''
|
||||
(() => {
|
||||
document.getElementById('editor').focus();
|
||||
const editor = document.getElementById('editor');
|
||||
if (editor && typeof editor.focus === 'function') {
|
||||
editor.focus();
|
||||
}
|
||||
})();''');
|
||||
} catch (e) {
|
||||
logWarning('RichTextMobileTabletController::focus:Exception: $e');
|
||||
|
||||
+22
-17
@@ -100,17 +100,17 @@ extension HandleAiScribeInComposerExtension on ComposerController {
|
||||
HtmlUtils.saveSelection.name,
|
||||
hasReturnValue: true,
|
||||
);
|
||||
return result;
|
||||
return result?.toString() ?? '';
|
||||
} else {
|
||||
final result = await richTextMobileTabletController?.htmlEditorApi?.webViewController
|
||||
.evaluateJavascript(
|
||||
source: HtmlUtils.saveSelection.script,
|
||||
);
|
||||
return result;
|
||||
return result?.toString() ?? '';
|
||||
}
|
||||
} catch (e) {
|
||||
logWarning('$runtimeType::saveSelection:Exception = $e');
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,17 +121,17 @@ extension HandleAiScribeInComposerExtension on ComposerController {
|
||||
HtmlUtils.restoreSelection.name,
|
||||
hasReturnValue: true,
|
||||
);
|
||||
return result;
|
||||
return result?.toString() ?? '';
|
||||
} else {
|
||||
final result = await richTextMobileTabletController?.htmlEditorApi?.webViewController
|
||||
.evaluateJavascript(
|
||||
source: HtmlUtils.restoreSelection.script,
|
||||
);
|
||||
return result;
|
||||
return result?.toString() ?? '';
|
||||
}
|
||||
} catch (e) {
|
||||
logWarning('$runtimeType::restoreSelection:Exception = $e');
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,17 +142,17 @@ extension HandleAiScribeInComposerExtension on ComposerController {
|
||||
HtmlUtils.getSavedSelection.name,
|
||||
hasReturnValue: true,
|
||||
);
|
||||
return result;
|
||||
return result?.toString() ?? '';
|
||||
} else {
|
||||
final result = await richTextMobileTabletController?.htmlEditorApi?.webViewController
|
||||
.evaluateJavascript(
|
||||
source: HtmlUtils.getSavedSelection.script,
|
||||
);
|
||||
return result;
|
||||
return result?.toString() ?? '';
|
||||
}
|
||||
} catch (e) {
|
||||
logWarning('$runtimeType::getSavedSelection:Exception = $e');
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,12 +176,16 @@ extension HandleAiScribeInComposerExtension on ComposerController {
|
||||
}
|
||||
|
||||
Future<void> unfocusEditor() async {
|
||||
final editorApi = richTextMobileTabletController?.htmlEditorApi;
|
||||
if (PlatformInfo.isIOS) {
|
||||
await editorApi?.unfocus();
|
||||
} else if (PlatformInfo.isAndroid) {
|
||||
await editorApi?.hideKeyboard();
|
||||
await editorApi?.unfocus();
|
||||
try {
|
||||
final editorApi = richTextMobileTabletController?.htmlEditorApi;
|
||||
if (PlatformInfo.isIOS) {
|
||||
await editorApi?.unfocus();
|
||||
} else if (PlatformInfo.isAndroid) {
|
||||
await editorApi?.hideKeyboard();
|
||||
await editorApi?.unfocus();
|
||||
}
|
||||
} catch (e) {
|
||||
logWarning('$runtimeType::unfocusEditor:Exception = $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +243,8 @@ extension HandleAiScribeInComposerExtension on ComposerController {
|
||||
clearFocusRecipients();
|
||||
clearFocusSubject();
|
||||
|
||||
if (isScribeMobile) {
|
||||
final scribeMobile = isScribeMobile;
|
||||
if (scribeMobile) {
|
||||
await saveAndUnfocusForModal();
|
||||
}
|
||||
|
||||
@@ -254,7 +259,7 @@ extension HandleAiScribeInComposerExtension on ComposerController {
|
||||
preferredPlacement: ModalPlacement.top,
|
||||
crossAxisAlignment: ModalCrossAxisAlignment.start,
|
||||
onSelectAiScribeSuggestionAction: handleAiScribeSuggestionAction,
|
||||
isScribeMobile: isScribeMobile,
|
||||
isScribeMobile: scribeMobile,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class GetLinagoraEcosystemSuccess extends Success {
|
||||
}
|
||||
|
||||
class GetLinagoraEcosystemFailure extends Failure {
|
||||
final dynamic exception;
|
||||
final Object exception;
|
||||
|
||||
GetLinagoraEcosystemFailure(this.exception);
|
||||
|
||||
|
||||
+1
@@ -3470,6 +3470,7 @@ class MailboxDashBoardController extends ReloadableController
|
||||
twakeAppManager.setHasComposer(false);
|
||||
paywallController?.onClose();
|
||||
paywallController = null;
|
||||
cachedLinagoraEcosystem = null;
|
||||
_disposeWorkerObxVariables();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
+4
-3
@@ -20,10 +20,10 @@ extension SetupScribePromptUrlExtension on MailboxDashBoardController {
|
||||
if (baseUrl != null && baseUrl.isNotEmpty) {
|
||||
consumeState(interactor.execute(baseUrl));
|
||||
} else {
|
||||
logError('SetupScribePromptUrlExtension::loadLinagoraEcosystem: jmapUrl is null or empty');
|
||||
logWarning('SetupScribePromptUrlExtension::loadLinagoraEcosystem: jmapUrl is null or empty');
|
||||
}
|
||||
} else {
|
||||
logError('SetupScribePromptUrlExtension::loadLinagoraEcosystem: GetLinagoraEcosystemInteractor not found');
|
||||
logWarning('SetupScribePromptUrlExtension::loadLinagoraEcosystem: GetLinagoraEcosystemInteractor not found');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ extension SetupScribePromptUrlExtension on MailboxDashBoardController {
|
||||
}
|
||||
|
||||
void handleGetLinagoraEcosystemFailure(GetLinagoraEcosystemFailure failure) {
|
||||
logError('SetupScribePromptUrlExtension::handleGetLinagoraEcosystemFailure: GetScribePromptUrl failed - ${failure.exception}');
|
||||
logWarning('SetupScribePromptUrlExtension::handleGetLinagoraEcosystemFailure: GetScribePromptUrl failed - ${failure.exception}');
|
||||
cachedLinagoraEcosystem = null;
|
||||
_applyScribePromptUrl(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ extension SessionExtension on Session {
|
||||
try {
|
||||
return personalAccount.accountId;
|
||||
} catch (e) {
|
||||
logError('SessionExtension::safeAccountId:Exception: $e');
|
||||
logWarning('SessionExtension::safeAccountId:Exception: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:scribe/scribe/ai/data/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_response.dart';
|
||||
|
||||
abstract class AIDataSource {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:scribe/scribe/ai/data/datasource/ai_datasource.dart';
|
||||
import 'package:scribe/scribe/ai/data/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/data/network/ai_api.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_response.dart';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:scribe/scribe/ai/data/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_message.dart';
|
||||
|
||||
class AIAPIRequest {
|
||||
final List<AIMessage> messages;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:core/data/network/dio_client.dart';
|
||||
import 'package:scribe/scribe/ai/data/model/ai_api_request.dart';
|
||||
import 'package:scribe/scribe/ai/data/model/ai_api_response.dart';
|
||||
import 'package:scribe/scribe/ai/data/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_message.dart';
|
||||
|
||||
class AIApi {
|
||||
final DioClient _dioClient;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:scribe/scribe/ai/data/datasource/ai_datasource.dart';
|
||||
import 'package:scribe/scribe/ai/data/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_response.dart';
|
||||
import 'package:scribe/scribe/ai/domain/repository/ai_scribe_repository.dart';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:scribe/scribe/ai/data/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/prompt_data.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
|
||||
@@ -59,8 +59,9 @@ class PromptService {
|
||||
}
|
||||
|
||||
Future<PromptData> _fetchPromptsFromUrl(String url) async {
|
||||
log('PromptService::_fetchPromptsFromUrl: Fetching from $url');
|
||||
|
||||
final sanitizedUrl =
|
||||
Uri.tryParse(url)?.replace(queryParameters: {}).toString() ?? url;
|
||||
log('PromptService::_fetchPromptsFromUrl: Fetching from $sanitizedUrl');
|
||||
try {
|
||||
final response = await _dio.get(url);
|
||||
final data = response.data;
|
||||
@@ -87,11 +88,11 @@ class PromptService {
|
||||
|
||||
Future<Prompt> getPromptByName(String name) async {
|
||||
final promptData = await loadPrompts();
|
||||
try {
|
||||
return promptData.prompts.firstWhere((prompt) => prompt.name == name);
|
||||
} catch (_) {
|
||||
final prompt = promptData.prompts.where((p) => p.name == name).firstOrNull;
|
||||
if (prompt == null) {
|
||||
throw Exception('Prompt not found: $name');
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
Future<List<AIMessage>> buildPromptByName(String name, String inputText, {String? task}) async {
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import 'package:scribe/scribe/ai/data/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_message.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/data/service/prompt_service.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class AIPrompts {
|
||||
static final PromptService _promptService = Get.find<PromptService>();
|
||||
static PromptService? _promptServiceInstance;
|
||||
|
||||
static PromptService get _promptService {
|
||||
if (!Get.isRegistered<PromptService>()) {
|
||||
throw StateError(
|
||||
'PromptService not registered. Ensure NetworkBindings.dependencies() has been called.');
|
||||
}
|
||||
return _promptServiceInstance ??= Get.find<PromptService>();
|
||||
}
|
||||
|
||||
static Future<List<AIMessage>> buildPrompt(AIAction action, String? text) async {
|
||||
return switch (action) {
|
||||
@@ -23,7 +31,17 @@ class AIPrompts {
|
||||
return await _promptService.buildPromptByName(menuAction.promptId, text);
|
||||
}
|
||||
|
||||
static Future<List<AIMessage>> buildCustomPrompt(String customPrompt, String? text) async {
|
||||
return await _promptService.buildPromptByName(CustomPromptAction.promptId, text ?? '', task: customPrompt);
|
||||
static Future<List<AIMessage>> buildCustomPrompt(
|
||||
String customPrompt,
|
||||
String? text,
|
||||
) async {
|
||||
if (customPrompt.trim().isEmpty) {
|
||||
throw ArgumentError('Custom prompt cannot be empty');
|
||||
}
|
||||
return await _promptService.buildPromptByName(
|
||||
CustomPromptAction.promptId,
|
||||
text ?? '',
|
||||
task: customPrompt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:scribe/scribe/ai/data/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_message.dart';
|
||||
|
||||
class PromptData {
|
||||
final List<Prompt> prompts;
|
||||
@@ -50,20 +50,18 @@ class Prompt {
|
||||
List<AIMessage> buildPrompt(String inputText, {String? task}) {
|
||||
return [
|
||||
for (final message in messages)
|
||||
if (message.role == AIRole.system)
|
||||
AIMessage.ofSystem(message.content)
|
||||
else if (message.role == AIRole.user)
|
||||
AIMessage.ofUser(_replacePlaceholders(message.content, inputText, task))
|
||||
switch (message.role) {
|
||||
AIRole.system => AIMessage.ofSystem(message.content),
|
||||
AIRole.user => AIMessage.ofUser(
|
||||
_replacePlaceholders(message.content, inputText, task),
|
||||
),
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
String _replacePlaceholders(String content, String inputText, String? task) {
|
||||
var result = content.replaceAll('{{input}}', inputText);
|
||||
|
||||
if (task != null) {
|
||||
result = result.replaceAll('{{task}}', task);
|
||||
}
|
||||
|
||||
var result = content.replaceAll(RegExp(r'\{\{\s*input\s*\}\}'), inputText);
|
||||
result = result.replaceAll(RegExp(r'\{\{\s*task\s*\}\}'), task ?? '');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:scribe/scribe/ai/data/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_response.dart';
|
||||
|
||||
abstract class AIScribeRepository {
|
||||
|
||||
@@ -120,13 +120,7 @@ abstract final class AIScribeTextStyles {
|
||||
color: Colors.black.withValues(alpha: 0.85),
|
||||
);
|
||||
|
||||
static final TextStyle contentCard =
|
||||
ThemeUtils.textStyleInter400.copyWith(
|
||||
fontSize: 14,
|
||||
height: 22 / 14,
|
||||
letterSpacing: 0.4,
|
||||
color: Colors.black.withValues(alpha: 0.85),
|
||||
);
|
||||
static final TextStyle contentCard = suggestionContent;
|
||||
|
||||
static final TextStyle mainActionButton =
|
||||
ThemeUtils.textStyleInter500().copyWith(
|
||||
@@ -187,7 +181,7 @@ abstract final class AIScribeSizes {
|
||||
static const double scribeIcon = 12;
|
||||
static const double scribeMobileIcon = 16;
|
||||
static const double aiAssistantIcon = 24;
|
||||
static const double bottomsheetIcon = 20;
|
||||
static const double bottomSheetIcon = 20;
|
||||
|
||||
// Button sizes
|
||||
static const double minButtonWidth = 72;
|
||||
|
||||
@@ -4,6 +4,9 @@ import 'package:get/get.dart';
|
||||
|
||||
class AiScribeMobileUtils {
|
||||
static bool isScribeInMobileMode(BuildContext? context) {
|
||||
return context != null && (Get.find<ResponsiveUtils>().isMobile(context) || Get.find<ResponsiveUtils>().isLandscapeMobile(context));
|
||||
if (context == null) return false;
|
||||
final responsiveUtils = Get.find<ResponsiveUtils>();
|
||||
return responsiveUtils.isMobile(context) ||
|
||||
responsiveUtils.isLandscapeMobile(context);
|
||||
}
|
||||
}
|
||||
+15
-8
@@ -22,14 +22,20 @@ mixin AiScribeSuggestionStateMixin<T extends StatefulWidget> on State<T> {
|
||||
ImagePaths get imagePaths;
|
||||
OnSelectAiScribeSuggestionAction get onSelectAction;
|
||||
|
||||
late AIAction _currentAiAction;
|
||||
String? _currentContent;
|
||||
int _requestId = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentAiAction = aiAction;
|
||||
_currentContent = content;
|
||||
|
||||
if (!Get.isRegistered<GenerateAITextInteractor>()) {
|
||||
_suggestionState.value = dartz.Left(
|
||||
GenerateAITextFailure(
|
||||
GenerateAITextInteractorIsNotRegisteredException(),
|
||||
const GenerateAITextInteractorIsNotRegisteredException(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -40,26 +46,27 @@ mixin AiScribeSuggestionStateMixin<T extends StatefulWidget> on State<T> {
|
||||
}
|
||||
|
||||
Future<void> loadSuggestion([AIAction? newAiAction, String? newContent]) async {
|
||||
final aiActionToSend = newAiAction ?? aiAction;
|
||||
final contentToSend = newContent ?? content;
|
||||
_currentAiAction = newAiAction ?? _currentAiAction;
|
||||
_currentContent = newContent ?? _currentContent;
|
||||
final requestId = ++_requestId;
|
||||
|
||||
_suggestionState.value = dartz.Right(GenerateAITextLoading());
|
||||
|
||||
if (_interactor == null) {
|
||||
_suggestionState.value = dartz.Left(
|
||||
GenerateAITextFailure(
|
||||
GenerateAITextInteractorIsNotRegisteredException(),
|
||||
const GenerateAITextInteractorIsNotRegisteredException(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await _interactor!.execute(
|
||||
aiActionToSend,
|
||||
contentToSend,
|
||||
_currentAiAction,
|
||||
_currentContent,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
if (!mounted || requestId != _requestId) return;
|
||||
|
||||
result.fold(
|
||||
(failure) => _suggestionState.value = dartz.Left(failure),
|
||||
@@ -77,7 +84,7 @@ mixin AiScribeSuggestionStateMixin<T extends StatefulWidget> on State<T> {
|
||||
(failure) => buildErrorState(),
|
||||
(value) {
|
||||
if (value is GenerateAITextSuccess) {
|
||||
final hasContent = content?.trim().isNotEmpty == true;
|
||||
final hasContent = _currentContent?.trim().isNotEmpty == true;
|
||||
|
||||
return buildSuccessState(
|
||||
value.response.result,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:core/presentation/resources/image_paths.dart';
|
||||
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
||||
import 'package:core/utils/platform_info.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:scribe/scribe.dart';
|
||||
@@ -21,7 +20,10 @@ class InlineAiAssistButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconSize = PlatformInfo.isWeb ? AIScribeSizes.scribeIcon : AIScribeSizes.scribeMobileIcon;
|
||||
final isScribeMobile = AiScribeMobileUtils.isScribeInMobileMode(context);
|
||||
final iconSize = isScribeMobile
|
||||
? AIScribeSizes.scribeMobileIcon
|
||||
: AIScribeSizes.scribeIcon;
|
||||
|
||||
return TMailButtonWidget.fromIcon(
|
||||
icon: imagePaths.icSparkle,
|
||||
@@ -31,11 +33,14 @@ class InlineAiAssistButton extends StatelessWidget {
|
||||
iconColor: AIScribeColors.scribeIcon,
|
||||
borderRadius: AIScribeSizes.scribeButtonRadius,
|
||||
boxShadow: AIScribeShadows.sparkleIcon,
|
||||
onTapActionCallback: () => _onTapActionCallback(context),
|
||||
onTapActionCallback: () => _onTapActionCallback(context, isScribeMobile),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onTapActionCallback(BuildContext context) async {
|
||||
Future<void> _onTapActionCallback(
|
||||
BuildContext context,
|
||||
bool isScribeMobile,
|
||||
) async {
|
||||
final renderBox = context.findRenderObject();
|
||||
|
||||
Offset? position;
|
||||
@@ -46,8 +51,6 @@ class InlineAiAssistButton extends StatelessWidget {
|
||||
size = renderBox.size;
|
||||
}
|
||||
|
||||
final isScribeMobile = AiScribeMobileUtils.isScribeInMobileMode(context);
|
||||
|
||||
await onTapFallback?.call();
|
||||
|
||||
await AiScribeModalManager.showAIScribeModal(
|
||||
|
||||
+13
-19
@@ -58,7 +58,7 @@ class _AiScribeMobileActionsBottomSheetState
|
||||
? TMailButtonWidget.fromIcon(
|
||||
icon: widget.imagePaths.icArrowBackIos,
|
||||
backgroundColor: Colors.transparent,
|
||||
iconSize: AIScribeSizes.bottomsheetIcon,
|
||||
iconSize: AIScribeSizes.bottomSheetIcon,
|
||||
iconColor: AIScribeColors.secondaryIcon,
|
||||
padding: AIScribeSizes.backIconPadding,
|
||||
onTapActionCallback: _goBackToCategories
|
||||
@@ -80,7 +80,7 @@ class _AiScribeMobileActionsBottomSheetState
|
||||
TMailButtonWidget.fromIcon(
|
||||
icon: widget.imagePaths.icCloseDialog,
|
||||
backgroundColor: Colors.transparent,
|
||||
iconSize: AIScribeSizes.bottomsheetIcon,
|
||||
iconSize: AIScribeSizes.bottomSheetIcon,
|
||||
iconColor: AIScribeColors.secondaryIcon,
|
||||
onTapActionCallback: () => Navigator.of(context).pop()
|
||||
)
|
||||
@@ -126,13 +126,7 @@ class _AiScribeMobileActionsBottomSheetState
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextCard(BuildContext context) {
|
||||
final displayText = widget.content;
|
||||
|
||||
if (displayText == null || displayText.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget _buildTextCard(String displayText) {
|
||||
return Container(
|
||||
margin: AIScribeSizes.contentCardMargin,
|
||||
constraints: const BoxConstraints(
|
||||
@@ -205,14 +199,14 @@ class _AiScribeMobileActionsBottomSheetState
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildHeader(context, localizations),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildTextCard(context),
|
||||
if(hasContent)
|
||||
if (hasContent)
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildTextCard(widget.content ?? ''),
|
||||
ValueListenableBuilder<AiScribeCategoryContextMenuAction?>(
|
||||
valueListenable: _selectedCategory,
|
||||
builder: (context, selectedCategory, _) {
|
||||
@@ -221,10 +215,10 @@ class _AiScribeMobileActionsBottomSheetState
|
||||
: _buildSubmenuListView();
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildBottomBar(context)
|
||||
],
|
||||
),
|
||||
|
||||
@@ -5,14 +5,14 @@ import 'package:scribe/scribe.dart';
|
||||
class AiScribeMobileActionsItem extends StatelessWidget {
|
||||
final AiScribeContextMenuAction menuAction;
|
||||
final ImagePaths imagePaths;
|
||||
final ValueChanged<AiScribeCategoryContextMenuAction>? onCategorySelected;
|
||||
final ValueChanged<AiScribeCategoryContextMenuAction> onCategorySelected;
|
||||
final ValueChanged<AiScribeContextMenuAction> onActionSelected;
|
||||
|
||||
const AiScribeMobileActionsItem({
|
||||
super.key,
|
||||
required this.menuAction,
|
||||
required this.imagePaths,
|
||||
this.onCategorySelected,
|
||||
required this.onCategorySelected,
|
||||
required this.onActionSelected,
|
||||
});
|
||||
|
||||
@@ -23,11 +23,11 @@ class AiScribeMobileActionsItem extends StatelessWidget {
|
||||
return AiScribeMenuItem(
|
||||
menuAction: menuAction,
|
||||
imagePaths: imagePaths,
|
||||
onSelectAction: (menuAction) {
|
||||
if (menuAction is AiScribeCategoryContextMenuAction) {
|
||||
onCategorySelected?.call(menuAction);
|
||||
onSelectAction: (selectedAction) {
|
||||
if (selectedAction is AiScribeCategoryContextMenuAction) {
|
||||
onCategorySelected.call(selectedAction);
|
||||
} else {
|
||||
onActionSelected.call(menuAction);
|
||||
onActionSelected.call(selectedAction);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -83,11 +83,17 @@ class AiScribeModalWidget extends StatelessWidget {
|
||||
// in tablet mode where we can encounter keyboard and modal we have issues
|
||||
// with calculating the modal height and the search bar is frequently behind the keyboard
|
||||
// that's why we take more space here
|
||||
final searchBarHeight = keyboardHeight > 0 ? AIScribeSizes.searchBarMaxHeight : AIScribeSizes.searchBarMinHeight;
|
||||
final searchBarHeight = showCustomPromptBar
|
||||
? (keyboardHeight > 0
|
||||
? AIScribeSizes.searchBarMaxHeight
|
||||
: AIScribeSizes.searchBarMinHeight)
|
||||
: 0.0;
|
||||
final contentSpacing =
|
||||
hasContent && showCustomPromptBar ? AIScribeSizes.fieldSpacing : 0.0;
|
||||
|
||||
final maxHeightModal = hasContent
|
||||
? searchBarHeight +
|
||||
AIScribeSizes.fieldSpacing +
|
||||
contentSpacing +
|
||||
min(menuActions.length * AIScribeSizes.menuItemHeight,
|
||||
AIScribeSizes.submenuMaxHeight)
|
||||
: searchBarHeight;
|
||||
|
||||
@@ -74,9 +74,12 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
|
||||
AIScribeSizes.suggestionModalMaxWidth,
|
||||
);
|
||||
|
||||
final modalMaxHeight = min(
|
||||
availableHeight * AIScribeSizes.mobileFactor,
|
||||
AIScribeSizes.suggestionModalMaxHeight,
|
||||
final modalMaxHeight = max(
|
||||
AIScribeSizes.suggestionModalMinHeight,
|
||||
min(
|
||||
availableHeight * AIScribeSizes.mobileFactor,
|
||||
AIScribeSizes.suggestionModalMaxHeight,
|
||||
),
|
||||
);
|
||||
|
||||
final dialogContent = _buildDialogContent(context);
|
||||
@@ -148,9 +151,12 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
|
||||
top = anchorPos.dy;
|
||||
bottom = null;
|
||||
|
||||
height = min(
|
||||
layout.availableHeight,
|
||||
screenSize.height - anchorPos.dy - anchorSize.height - _defaultPadding,
|
||||
height = max(
|
||||
AIScribeSizes.suggestionModalMinHeight,
|
||||
min(
|
||||
layout.availableHeight,
|
||||
screenSize.height - anchorPos.dy - anchorSize.height - _defaultPadding,
|
||||
),
|
||||
);
|
||||
|
||||
width = min(
|
||||
@@ -163,7 +169,10 @@ class _AiScribeSuggestionWidgetState extends State<AiScribeSuggestionWidget>
|
||||
// Layout bottom doesn't account for keyboard in calculation usually, so we add it back
|
||||
bottom = layout.bottom + keyboardHeightWithSpacing;
|
||||
|
||||
height = layout.availableHeight;
|
||||
height = max(
|
||||
AIScribeSizes.suggestionModalMinHeight,
|
||||
layout.availableHeight,
|
||||
);
|
||||
width = modalWidth;
|
||||
}
|
||||
|
||||
|
||||
+34
-26
@@ -46,19 +46,39 @@ class AiScribeSuggestionSuccessActions extends StatelessWidget {
|
||||
|
||||
Widget _buildReplaceButton(BuildContext context) {
|
||||
final localizations = ScribeLocalizations.of(context);
|
||||
return _buildActionButton(
|
||||
context: context,
|
||||
label: AiScribeSuggestionActions.replace.getLabel(localizations),
|
||||
textColor: AppColor.primaryMain,
|
||||
action: AiScribeSuggestionActions.replace,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton({
|
||||
required BuildContext context,
|
||||
required String label,
|
||||
Color? backgroundColor,
|
||||
required Color textColor,
|
||||
required AiScribeSuggestionActions action,
|
||||
}) {
|
||||
final isMobileScribe = AiScribeMobileUtils.isScribeInMobileMode(context);
|
||||
return Flexible(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(minWidth: AIScribeSizes.minButtonWidth),
|
||||
height: AIScribeSizes.buttonHeight,
|
||||
constraints: BoxConstraints(
|
||||
minWidth: isMobileScribe
|
||||
? AIScribeSizes.minButtonMobileWidth
|
||||
: AIScribeSizes.minButtonWidth,
|
||||
),
|
||||
height: isMobileScribe
|
||||
? AIScribeSizes.buttonMobileHeight
|
||||
: AIScribeSizes.buttonHeight,
|
||||
child: ConfirmDialogButton(
|
||||
label: AiScribeSuggestionActions.replace.getLabel(localizations),
|
||||
textColor: AppColor.primaryMain,
|
||||
label: label,
|
||||
backgroundColor: backgroundColor,
|
||||
textColor: textColor,
|
||||
onTapAction: () {
|
||||
Navigator.of(context).pop();
|
||||
onSelectAction(
|
||||
AiScribeSuggestionActions.replace,
|
||||
suggestionText,
|
||||
);
|
||||
onSelectAction(action, suggestionText);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -67,24 +87,12 @@ class AiScribeSuggestionSuccessActions extends StatelessWidget {
|
||||
|
||||
Widget _buildInsertButton(BuildContext context) {
|
||||
final localizations = ScribeLocalizations.of(context);
|
||||
final isMobileScribe = AiScribeMobileUtils.isScribeInMobileMode(context);
|
||||
return Flexible(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(minWidth: isMobileScribe ? AIScribeSizes.minButtonMobileWidth : AIScribeSizes.minButtonWidth),
|
||||
height: isMobileScribe ? AIScribeSizes.buttonMobileHeight : AIScribeSizes.buttonHeight,
|
||||
child: ConfirmDialogButton(
|
||||
label: AiScribeSuggestionActions.insert.getLabel(localizations),
|
||||
backgroundColor: AppColor.primaryMain,
|
||||
textColor: Colors.white,
|
||||
onTapAction: () {
|
||||
Navigator.of(context).pop();
|
||||
onSelectAction(
|
||||
AiScribeSuggestionActions.insert,
|
||||
suggestionText,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
return _buildActionButton(
|
||||
context: context,
|
||||
label: AiScribeSuggestionActions.insert.getLabel(localizations),
|
||||
backgroundColor: AppColor.primaryMain,
|
||||
textColor: Colors.white,
|
||||
action: AiScribeSuggestionActions.insert,
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -20,7 +20,7 @@ class AiScribeSuggestionSuccessToolbar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appToast = Get.find<AppToast>();
|
||||
final appToast = Get.isRegistered<AppToast>() ? Get.find<AppToast>() : null;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@@ -33,7 +33,7 @@ class AiScribeSuggestionSuccessToolbar extends StatelessWidget {
|
||||
tooltipMessage: ScribeLocalizations.of(context).copy,
|
||||
onTapActionCallback: () {
|
||||
Clipboard.setData(ClipboardData(text: suggestionText));
|
||||
appToast.showToastSuccessMessage(
|
||||
appToast?.showToastSuccessMessage(
|
||||
context,
|
||||
ScribeLocalizations.of(context).copiedToClipboard,
|
||||
);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:scribe/scribe/ai/data/service/prompt_service.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/prompt_data.dart';
|
||||
import 'package:scribe/scribe/ai/data/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_message.dart';
|
||||
|
||||
class _ThrowingAdapter implements HttpClientAdapter {
|
||||
@override
|
||||
@@ -59,51 +58,21 @@ void main() {
|
||||
});
|
||||
|
||||
group('PromptService getPromptByName', () {
|
||||
test('getPromptByName should return correct prompt from data', () async {
|
||||
// Arrange
|
||||
final promptData = PromptData(
|
||||
prompts: [
|
||||
Prompt(
|
||||
name: 'test-prompt',
|
||||
messages: [
|
||||
const AIMessage(role: AIRole.system, content: 'System message'),
|
||||
const AIMessage(role: AIRole.user, content: 'User message with {{input}}')
|
||||
]
|
||||
)
|
||||
]
|
||||
);
|
||||
test('getPromptByName should return correct prompt from assets', () async {
|
||||
final service = PromptService(_throwingDio());
|
||||
|
||||
// Act
|
||||
final prompt = promptData.prompts.firstWhere(
|
||||
(prompt) => prompt.name == 'test-prompt',
|
||||
orElse: () => throw Exception('Prompt not found: test-prompt'),
|
||||
);
|
||||
final prompt = await service.getPromptByName('change-tone-casual');
|
||||
|
||||
// Assert
|
||||
expect(prompt.name, 'test-prompt');
|
||||
expect(prompt.messages.length, 2);
|
||||
expect(prompt.name, 'change-tone-casual');
|
||||
expect(prompt.messages.length, greaterThan(0));
|
||||
});
|
||||
|
||||
test('getPromptByName should throw exception for non-existent prompt', () async {
|
||||
// Arrange
|
||||
final promptData = PromptData(
|
||||
prompts: [
|
||||
Prompt(
|
||||
name: 'test-prompt',
|
||||
messages: [
|
||||
const AIMessage(role: AIRole.system, content: 'System message'),
|
||||
const AIMessage(role: AIRole.user, content: 'User message')
|
||||
]
|
||||
)
|
||||
]
|
||||
);
|
||||
test('getPromptByName should throw exception for non-existent prompt',
|
||||
() async {
|
||||
final service = PromptService(_throwingDio());
|
||||
|
||||
// Act & Assert
|
||||
expect(
|
||||
() => promptData.prompts.firstWhere(
|
||||
(prompt) => prompt.name == 'non-existent-prompt',
|
||||
orElse: () => throw Exception('Prompt not found: non-existent-prompt'),
|
||||
),
|
||||
await expectLater(
|
||||
service.getPromptByName('non-existent-prompt'),
|
||||
throwsException,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/prompt_data.dart';
|
||||
import 'package:scribe/scribe/ai/data/model/ai_message.dart';
|
||||
import 'package:scribe/scribe/ai/domain/model/ai_message.dart';
|
||||
|
||||
void main() {
|
||||
group('PromptData', () {
|
||||
@@ -163,5 +163,19 @@ void main() {
|
||||
expect(result.length, 2);
|
||||
expect(result.last.content, 'User message without placeholders');
|
||||
});
|
||||
|
||||
test('fromJson should throw FormatException when name is missing', () {
|
||||
final jsonData = {"messages": []};
|
||||
|
||||
expect(() => Prompt.fromJson(jsonData), throwsA(isA<FormatException>()));
|
||||
});
|
||||
|
||||
test('fromJson should handle missing prompts key', () {
|
||||
final jsonData = <String, dynamic>{};
|
||||
|
||||
final promptData = PromptData.fromJson(jsonData);
|
||||
|
||||
expect(promptData.prompts, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user