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