diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index afa1e8eb..efdcc4ac 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/example/android/settings.gradle.kts b/example/android/settings.gradle.kts index a5112728..e693512a 100644 --- a/example/android/settings.gradle.kts +++ b/example/android/settings.gradle.kts @@ -18,7 +18,7 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.7.0" apply false + id("com.android.application") version "8.9.1" apply false // START: FlutterFire Configuration id("com.google.gms.google-services") version("4.3.15") apply false // END: FlutterFire Configuration diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf76..391a902b 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 818227d5..99c72b2a 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -5,6 +5,8 @@ PODS: - PromisesObjC (~> 2.4) - camera_avfoundation (0.0.1): - Flutter + - device_info_plus (0.0.1): + - Flutter - file_selector_ios (0.0.1): - Flutter - Firebase/Auth (12.4.0): @@ -77,6 +79,8 @@ PODS: - GTMSessionFetcher/Core (5.0.0) - image_picker_ios (0.0.1): - Flutter + - irondash_engine_context (0.0.1): + - Flutter - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS @@ -87,20 +91,25 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - super_native_extensions (0.0.1): + - Flutter - url_launcher_ios (0.0.1): - Flutter DEPENDENCIES: - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`) - firebase_app_check (from `.symlinks/plugins/firebase_app_check/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - Flutter (from `Flutter`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - record_ios (from `.symlinks/plugins/record_ios/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -122,6 +131,8 @@ SPEC REPOS: EXTERNAL SOURCES: camera_avfoundation: :path: ".symlinks/plugins/camera_avfoundation/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" file_selector_ios: :path: ".symlinks/plugins/file_selector_ios/ios" firebase_app_check: @@ -134,18 +145,23 @@ EXTERNAL SOURCES: :path: Flutter image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" + irondash_engine_context: + :path: ".symlinks/plugins/irondash_engine_context/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" record_ios: :path: ".symlinks/plugins/record_ios/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + super_native_extensions: + :path: ".symlinks/plugins/super_native_extensions/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f camera_avfoundation: 281867ff09f1da66f031a184ecfbc6f2e625c9f5 + device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 file_selector_ios: 80c12e90ad3f2045ed6819d03742f1a4c5ec3f93 Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e firebase_app_check: 53a9efd793edad49230d8d49b19cb8d47b8450ed @@ -162,11 +178,13 @@ SPEC CHECKSUMS: GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GTMSessionFetcher: 02d6e866e90bc236f48a703a041dfe43e6221a29 image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b + irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba record_ios: 840d21cce013c5a3b2168b74a54ebdb4136359e2 shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 + super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa PODFILE CHECKSUM: 7773a3d1e948b3cef227c6713241e4fcfe42cda9 diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 62666446..c30b367e 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -2,12 +2,15 @@ import Flutter import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index edcf9af3..d9bfba1c 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -30,6 +32,27 @@ $(PRODUCT_NAME) would like to access your microphone. NSPhotoLibraryUsageDescription $(PRODUCT_NAME) would like access to your photos. + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName @@ -49,9 +72,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/example/lib/custom_strings/custom_strings.dart b/example/lib/custom_strings/custom_strings.dart new file mode 100644 index 00000000..da1749a8 --- /dev/null +++ b/example/lib/custom_strings/custom_strings.dart @@ -0,0 +1,102 @@ +// Copyright 2025 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:firebase_ai/firebase_ai.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; + +import '../firebase_options.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + runApp(const App()); +} + +class App extends StatelessWidget { + static const title = 'Example: Google Gemini AI'; + + const App({super.key}); + + @override + Widget build(BuildContext context) => + const MaterialApp(title: title, home: CustomStringsExample()); +} + +class CustomStringsExample extends StatelessWidget { + static const title = 'Custom Chat Strings'; + + const CustomStringsExample({super.key}); + + @override + Widget build(BuildContext context) { + final customStrings = const LlmChatViewStrings( + addAttachment: 'Add', + attachFile: 'Attach File', + takePhoto: 'Take Photo', + stop: 'âšī¸ Stop', + close: '❌ Close', + cancel: '❌ Cancel', + copyToClipboard: '📋 Copy', + editMessage: 'âœī¸ Edit', + attachImage: 'đŸ–ŧī¸ Add Image', + recordAudio: '🎤 Record', + submitMessage: '📤 Send', + closeMenu: '❌ Close Menu', + + // Message related + typeAMessage: 'Type your message here...', + recording: '🔴 Recording...', + tapToStop: 'Tap to stop', + tapToRecord: 'Tap to record', + releaseToCancel: 'Release to cancel', + slideToCancel: 'Slide to cancel', + + submit: 'Submit', + send: 'Send', + delete: 'đŸ—‘ī¸ Delete', + edit: 'âœī¸ Edit', + copy: '📋 Copy', + share: 'â†—ī¸ Share', + retry: '🔄 Retry', + yes: '✅ Yes', + no: '❌ No', + clear: 'đŸ—‘ī¸ Clear', + search: '🔍 Search', + + // Messages and errors + messageCopiedToClipboard: '📋 Copied to clipboard!', + editing: 'âœī¸ Editing', + error: '❌ Error', + cancelMessage: 'Cancel', + confirmDelete: 'Confirm Delete', + areYouSureYouWantToDeleteThisMessage: + 'Are you sure you want to delete this message?', + errorSendingMessage: '❌ Failed to send message', + errorLoadingMessages: '❌ Failed to load messages', + noMessagesYet: 'No messages yet. Start the conversation!', + tapToRetry: 'Tap to retry', + noResultsFound: 'No results found', + unableToRecordAudio: 'Unable to record audio', + unsupportedImageSource: 'Unsupported image source', + unableToPickImage: 'Unable to pick image', + unableToPickFile: 'Unable to pick file', + unableToPickUrl: 'Unable to process URL', + ); + + return Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: LlmChatView( + provider: FirebaseProvider( + model: FirebaseAI.googleAI().generativeModel( + model: 'gemini-2.0-flash', + ), + ), + strings: customStrings, + style: LlmChatViewStyle(strings: customStrings), + ), + ); + } +} diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 20c23bad..eca53e6e 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,22 +5,30 @@ import FlutterMacOS import Foundation +import device_info_plus import file_selector_macos import firebase_app_check import firebase_auth import firebase_core +import irondash_engine_context +import mac_menu_bar import path_provider_foundation import record_macos import shared_preferences_foundation +import super_native_extensions import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) + MacMenuBarPlugin.register(with: registry.registrar(forPlugin: "MacMenuBarPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index f5abe519..c59f086a 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -3,6 +3,8 @@ PODS: - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - PromisesObjC (~> 2.4) + - device_info_plus (0.0.1): + - FlutterMacOS - file_selector_macos (0.0.1): - FlutterMacOS - Firebase/AppCheck (12.4.0): @@ -77,6 +79,10 @@ PODS: - GoogleUtilities/Logger - GoogleUtilities/Privacy - GTMSessionFetcher/Core (5.0.0) + - irondash_engine_context (0.0.1): + - FlutterMacOS + - mac_menu_bar (0.0.1): + - FlutterMacOS - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS @@ -86,18 +92,24 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - super_native_extensions (0.0.1): + - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS DEPENDENCIES: + - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - firebase_app_check (from `Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos`) - firebase_auth (from `Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos`) - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`) + - mac_menu_bar (from `Flutter/ephemeral/.symlinks/plugins/mac_menu_bar/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) SPEC REPOS: @@ -116,6 +128,8 @@ SPEC REPOS: - PromisesObjC EXTERNAL SOURCES: + device_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos file_selector_macos: :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos firebase_app_check: @@ -126,17 +140,24 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos FlutterMacOS: :path: Flutter/ephemeral + irondash_engine_context: + :path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos + mac_menu_bar: + :path: Flutter/ephemeral/.symlinks/plugins/mac_menu_bar/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin record_macos: :path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + super_native_extensions: + :path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f + device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 file_selector_macos: 3e56eaea051180007b900eacb006686fd54da150 Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e firebase_app_check: 87116ccdfe0f153231af37b0431e96b0d5a76b9c @@ -152,10 +173,13 @@ SPEC CHECKSUMS: FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GTMSessionFetcher: 02d6e866e90bc236f48a703a041dfe43e6221a29 + irondash_engine_context: da62996ee25616d2f01bbeb85dc115d813359478 + mac_menu_bar: 9290444652836c996e840adeaac5bff963673507 path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 record_macos: 4440ca269ad3b870ebb1965297a365d558f0c520 shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 + super_native_extensions: 85efee3a7495b46b04befcfc86ed12069264ebf3 url_launcher_macos: 175a54c831f4375a6cf895875f716ee5af3888ce PODFILE CHECKSUM: abc7d4662afc18f3dac224359a4bbdfd943487c9 diff --git a/lib/flutter_ai_toolkit.dart b/lib/flutter_ai_toolkit.dart index 96ad500d..de468e07 100644 --- a/lib/flutter_ai_toolkit.dart +++ b/lib/flutter_ai_toolkit.dart @@ -19,3 +19,4 @@ export 'src/providers/interface/chat_message.dart'; export 'src/providers/providers.dart'; export 'src/styles/styles.dart'; export 'src/views/llm_chat_view/llm_chat_view.dart'; +export 'src/strings/strings.dart'; diff --git a/lib/src/chat_view_model/chat_view_model.dart b/lib/src/chat_view_model/chat_view_model.dart index e44e089f..c27663f5 100644 --- a/lib/src/chat_view_model/chat_view_model.dart +++ b/lib/src/chat_view_model/chat_view_model.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import '../providers/interface/llm_provider.dart'; +import '../strings/llm_chat_view_strings.dart'; import '../styles/llm_chat_view_style.dart'; import '../views/response_builder.dart'; @@ -35,6 +36,7 @@ class ChatViewModel { required this.speechToText, required this.enableAttachments, required this.enableVoiceNotes, + this.strings = const LlmChatViewStrings(), }); /// The LLM provider for the chat interface. @@ -92,6 +94,12 @@ class ChatViewModel { /// will be disabled. final bool enableVoiceNotes; + /// The strings used throughout the chat interface. + /// + /// This provides access to all the text strings used in the chat interface, + /// allowing for easy customization and internationalization. + final LlmChatViewStrings strings; + // The following is needed to support the // ChatViewModelProvider.updateShouldNotify implementation @override @@ -104,8 +112,10 @@ class ChatViewModel { other.welcomeMessage == welcomeMessage && other.responseBuilder == responseBuilder && other.messageSender == messageSender && + other.speechToText == speechToText && other.enableAttachments == enableAttachments && - other.enableVoiceNotes == enableVoiceNotes); + other.enableVoiceNotes == enableVoiceNotes && + other.strings == strings); // the following is best practices when overriding operator == @override @@ -116,7 +126,9 @@ class ChatViewModel { welcomeMessage, responseBuilder, messageSender, + speechToText, enableAttachments, enableVoiceNotes, + strings, ); } diff --git a/lib/src/strings/llm_chat_view_strings.dart b/lib/src/strings/llm_chat_view_strings.dart new file mode 100644 index 00000000..0bc100cc --- /dev/null +++ b/lib/src/strings/llm_chat_view_strings.dart @@ -0,0 +1,358 @@ +// Copyright 2024 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +/// A class that contains all the strings used in the LlmChatView. +/// +/// This class provides a way to customize the text displayed in the chat interface. +/// You can use the default values or provide your own custom strings. +@immutable +class LlmChatViewStrings { + /// Default instance with all default values + static const LlmChatViewStrings defaults = LlmChatViewStrings(); + + /// Input/Attachment related strings + /// Text for the add attachment button. + final String addAttachment; + + /// Label for attaching a file. + final String attachFile; + + /// Label for taking a photo. + final String takePhoto; + + /// Text for the stop button. + final String stop; + + /// Text for the close button. + final String close; + + /// Text for the cancel button. + final String cancel; + + /// Text for the copy to clipboard action. + final String copyToClipboard; + + /// Text for the edit message action. + final String editMessage; + + /// Label for attaching an image. + final String attachImage; + + /// Text for the record audio button. + final String recordAudio; + + /// Text for the submit message button. + final String submitMessage; + + /// Text for closing a menu. + final String closeMenu; + + /// Error message when unable to record audio. + final String unableToRecordAudio; + + /// Error message prefix for unsupported image sources. + final String unsupportedImageSource; + + /// Error message prefix when unable to pick an image. + final String unableToPickImage; + + /// Error message prefix when unable to pick a file. + final String unableToPickFile; + + /// Error message prefix when unable to pick a url. + final String unableToPickUrl; + + /// Confirmation message when a message is copied to clipboard. + final String messageCopiedToClipboard; + + /// Label indicating editing mode. + final String editing; + + /// Generic error message. + final String error; + + /// Text for cancel action in dialogs. + final String cancelMessage; + + /// Text for submit action. + final String submit; + + /// Text for the send button. + final String send; + + /// Placeholder text for the message input field. + final String typeAMessage; + + /// Label shown during audio recording. + final String recording; + + /// Instruction to stop recording audio. + final String tapToStop; + + /// Instruction to start recording audio. + final String tapToRecord; + + /// Instruction shown when dragging to cancel recording. + final String releaseToCancel; + + /// Instruction shown when sliding to cancel an action. + final String slideToCancel; + + /// Text for the delete action. + final String delete; + + /// Title for the delete confirmation dialog. + final String confirmDelete; + + /// Confirmation message for message deletion. + final String areYouSureYouWantToDeleteThisMessage; + + /// Affirmative response text (e.g., 'YES', 'OK'). + final String yes; + + /// Negative response text (e.g., 'NO', 'Cancel'). + final String no; + + /// Text for the edit action. + final String edit; + + /// Text for the copy action. + final String copy; + + /// Text for the share action. + final String share; + + /// Text for the retry action. + final String retry; + + /// Error message when failing to send a message. + final String errorSendingMessage; + + /// Error message when failing to load messages. + final String errorLoadingMessages; + + /// Placeholder text when there are no messages. + final String noMessagesYet; + + /// Instruction to retry a failed action. + final String tapToRetry; + + /// Label for the search functionality. + final String search; + + /// Text for clearing input or search. + final String clear; + + /// Message shown when no search results are found. + final String noResultsFound; + + /// Creates a new instance of [LlmChatViewStrings] with the given strings. + /// + /// All parameters are optional and will default to the provided values. + const LlmChatViewStrings({ + // Input/Attachment related + this.addAttachment = 'Add Attachment', + this.attachFile = 'Attach File', + this.takePhoto = 'Take Photo', + this.attachImage = 'Attach Image', + this.recordAudio = 'Record Audio', + this.typeAMessage = 'Type a message...', + this.recording = 'Recording...', + this.tapToStop = 'Tap to stop', + this.tapToRecord = 'Tap to record', + this.releaseToCancel = 'Release to cancel', + this.slideToCancel = 'Slide to cancel', + + // Common actions + this.stop = 'Stop', + this.close = 'Close', + this.cancel = 'Cancel', + this.submit = 'Submit', + this.send = 'Send', + this.delete = 'Delete', + this.edit = 'Edit', + this.copy = 'Copy', + this.share = 'Share', + this.retry = 'Retry', + this.yes = 'Yes', + this.no = 'No', + this.clear = 'Clear', + this.search = 'Search', + + // Messages and errors + this.copyToClipboard = 'Copy to Clipboard', + this.editMessage = 'Edit Message', + this.submitMessage = 'Submit Message', + this.closeMenu = 'Close Menu', + this.unableToRecordAudio = 'Unable to record audio', + this.unsupportedImageSource = 'Unsupported image source: ', + this.unableToPickImage = 'Unable to pick an image: ', + this.unableToPickFile = 'Unable to pick a file: ', + this.unableToPickUrl = 'Unable to pick a URL: ', + this.messageCopiedToClipboard = 'Message copied to clipboard', + this.editing = 'Editing', + this.error = 'Error', + this.cancelMessage = 'Cancel', + this.confirmDelete = 'Confirm Delete', + this.areYouSureYouWantToDeleteThisMessage = + 'Are you sure you want to delete this message?', + this.errorSendingMessage = 'Error sending message', + this.errorLoadingMessages = 'Error loading messages', + this.noMessagesYet = 'No messages yet', + this.tapToRetry = 'Tap to retry', + this.noResultsFound = 'No results found', + }); + + /// Creates a copy of this [LlmChatViewStrings] with the given fields replaced + /// with the new values. + LlmChatViewStrings copyWith({ + String? addAttachment, + String? attachFile, + String? takePhoto, + String? stop, + String? close, + String? cancel, + String? copyToClipboard, + String? editMessage, + String? attachImage, + String? recordAudio, + String? submitMessage, + String? closeMenu, + String? unableToRecordAudio, + String? unsupportedImageSource, + String? unableToPickImage, + String? unableToPickFile, + String? unableToPickUrl, + String? messageCopiedToClipboard, + String? editing, + String? error, + String? cancelMessage, + String? submit, + String? send, + String? typeAMessage, + String? recording, + String? tapToStop, + String? tapToRecord, + String? releaseToCancel, + String? slideToCancel, + String? delete, + String? confirmDelete, + String? areYouSureYouWantToDeleteThisMessage, + String? yes, + String? no, + String? edit, + String? copy, + String? share, + String? retry, + String? errorSendingMessage, + String? errorLoadingMessages, + String? noMessagesYet, + String? tapToRetry, + String? search, + String? clear, + String? noResultsFound, + String? today, + String? yesterday, + String? lastWeek, + String? older, + }) { + return LlmChatViewStrings( + addAttachment: addAttachment ?? this.addAttachment, + attachFile: attachFile ?? this.attachFile, + takePhoto: takePhoto ?? this.takePhoto, + stop: stop ?? this.stop, + close: close ?? this.close, + cancel: cancel ?? this.cancel, + copyToClipboard: copyToClipboard ?? this.copyToClipboard, + editMessage: editMessage ?? this.editMessage, + attachImage: attachImage ?? this.attachImage, + recordAudio: recordAudio ?? this.recordAudio, + submitMessage: submitMessage ?? this.submitMessage, + closeMenu: closeMenu ?? this.closeMenu, + unableToRecordAudio: unableToRecordAudio ?? this.unableToRecordAudio, + unsupportedImageSource: + unsupportedImageSource ?? this.unsupportedImageSource, + unableToPickImage: unableToPickImage ?? this.unableToPickImage, + unableToPickFile: unableToPickFile ?? this.unableToPickFile, + unableToPickUrl: unableToPickUrl ?? this.unableToPickUrl, + messageCopiedToClipboard: + messageCopiedToClipboard ?? this.messageCopiedToClipboard, + editing: editing ?? this.editing, + error: error ?? this.error, + cancelMessage: cancelMessage ?? this.cancelMessage, + submit: submit ?? this.submit, + send: send ?? this.send, + typeAMessage: typeAMessage ?? this.typeAMessage, + recording: recording ?? this.recording, + tapToStop: tapToStop ?? this.tapToStop, + tapToRecord: tapToRecord ?? this.tapToRecord, + releaseToCancel: releaseToCancel ?? this.releaseToCancel, + slideToCancel: slideToCancel ?? this.slideToCancel, + delete: delete ?? this.delete, + confirmDelete: confirmDelete ?? this.confirmDelete, + areYouSureYouWantToDeleteThisMessage: + areYouSureYouWantToDeleteThisMessage ?? + this.areYouSureYouWantToDeleteThisMessage, + yes: yes ?? this.yes, + no: no ?? this.no, + edit: edit ?? this.edit, + copy: copy ?? this.copy, + share: share ?? this.share, + retry: retry ?? this.retry, + errorSendingMessage: errorSendingMessage ?? this.errorSendingMessage, + errorLoadingMessages: errorLoadingMessages ?? this.errorLoadingMessages, + noMessagesYet: noMessagesYet ?? this.noMessagesYet, + tapToRetry: tapToRetry ?? this.tapToRetry, + search: search ?? this.search, + clear: clear ?? this.clear, + noResultsFound: noResultsFound ?? this.noResultsFound, + ); + } + + /// Formats [source] into a string that describes an unsupported image source. + /// + /// The formatted string includes the value of [source] and the string + /// representation of [unsupportedImageSource]. + /// + /// The [source] parameter is the image source that is not supported. + /// + /// Returns a string that describes the unsupported image source. + String formatUnsupportedImageSource(String source) => + '$unsupportedImageSource: $source'; + + /// Formats [error] into a string that describes an error occurred while + /// picking an image. + /// + /// The formatted string includes the value of [error] and the string + /// representation of [unableToPickImage]. + /// + /// The [error] parameter is the error that occurred. + /// + /// Returns a string that describes the error. + String formatUnableToPickImage(String error) => '$unableToPickImage: $error'; + + /// Formats [error] into a string that describes an error occurred while + /// picking a file. + /// + /// The formatted string includes the value of [error] and the string + /// representation of [unableToPickFile]. + /// + /// The [error] parameter is the error that occurred. + /// + /// Returns a string that describes the error. + String formatUnableToPickFile(String error) => '$unableToPickFile: $error'; + + /// Formats [error] into a string that describes an error occurred while + /// picking a url. + /// + /// The formatted string includes the value of [error] and the string + /// representation of [unableToPickUrl]. + /// + /// The [error] parameter is the error that occurred. + /// + /// Returns a string that describes the error. + String formatUnableToPickUrl(String error) => '$unableToPickUrl: $error'; +} diff --git a/lib/src/strings/strings.dart b/lib/src/strings/strings.dart new file mode 100644 index 00000000..281f8376 --- /dev/null +++ b/lib/src/strings/strings.dart @@ -0,0 +1 @@ +export 'llm_chat_view_strings.dart'; diff --git a/lib/src/styles/action_button_style.dart b/lib/src/styles/action_button_style.dart index 4e86875f..93999716 100644 --- a/lib/src/styles/action_button_style.dart +++ b/lib/src/styles/action_button_style.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; +import '../strings/llm_chat_view_strings.dart'; import 'action_button_type.dart'; import 'tookit_icons.dart'; import 'toolkit_colors.dart'; @@ -42,11 +43,19 @@ class ActionButtonStyle { ); /// Provides default style for icon buttons. - factory ActionButtonStyle.defaultStyle(ActionButtonType type) => - ActionButtonStyle._lightStyle(type); + factory ActionButtonStyle.defaultStyle( + ActionButtonType type, { + LlmChatViewStrings? strings, + }) { + final resolvedStrings = strings ?? LlmChatViewStrings.defaults; + return ActionButtonStyle._lightStyle(type, resolvedStrings); + } /// Provides default light style for icon buttons. - factory ActionButtonStyle._lightStyle(ActionButtonType type) { + factory ActionButtonStyle._lightStyle( + ActionButtonType type, + LlmChatViewStrings strings, + ) { IconData? icon; var color = ToolkitColors.darkIcon; var bgColor = ToolkitColors.lightButtonBackground; @@ -56,56 +65,56 @@ class ActionButtonStyle { switch (type) { case ActionButtonType.add: icon = ToolkitIcons.add; - text = 'Add Attachment'; + text = strings.addAttachment; case ActionButtonType.attachFile: icon = ToolkitIcons.attach_file; color = ToolkitColors.darkIcon; bgColor = ToolkitColors.transparent; - text = 'Attach File'; + text = strings.attachFile; textStyle = ToolkitTextStyles.body2; case ActionButtonType.camera: icon = ToolkitIcons.camera_alt; color = ToolkitColors.darkIcon; bgColor = ToolkitColors.transparent; - text = 'Take Photo'; + text = strings.takePhoto; textStyle = ToolkitTextStyles.body2; case ActionButtonType.stop: icon = ToolkitIcons.stop; - text = 'Stop'; + text = strings.stop; case ActionButtonType.close: icon = ToolkitIcons.close; color = ToolkitColors.whiteIcon; bgColor = ToolkitColors.darkButtonBackground; - text = 'Close'; + text = strings.close; case ActionButtonType.cancel: icon = ToolkitIcons.close; color = ToolkitColors.whiteIcon; bgColor = ToolkitColors.darkButtonBackground; - text = 'Cancel'; + text = strings.cancel; case ActionButtonType.copy: icon = ToolkitIcons.content_copy; color = ToolkitColors.whiteIcon; bgColor = ToolkitColors.darkButtonBackground; - text = 'Copy to Clipboard'; + text = strings.copyToClipboard; case ActionButtonType.edit: icon = ToolkitIcons.edit; color = ToolkitColors.whiteIcon; bgColor = ToolkitColors.darkButtonBackground; - text = 'Edit Message'; + text = strings.editMessage; case ActionButtonType.gallery: icon = ToolkitIcons.image; color = ToolkitColors.darkIcon; bgColor = ToolkitColors.transparent; - text = 'Attach Image'; + text = strings.attachImage; textStyle = ToolkitTextStyles.body2; case ActionButtonType.record: icon = ToolkitIcons.mic; - text = 'Record Audio'; + text = strings.recordAudio; case ActionButtonType.submit: icon = ToolkitIcons.submit_icon; color = ToolkitColors.whiteIcon; bgColor = ToolkitColors.darkButtonBackground; - text = 'Submit Message'; + text = strings.submitMessage; case ActionButtonType.disabled: icon = ToolkitIcons.submit_icon; color = ToolkitColors.darkIcon; @@ -115,12 +124,12 @@ class ActionButtonStyle { icon = ToolkitIcons.close; color = ToolkitColors.whiteIcon; bgColor = ToolkitColors.greyBackground; - text = 'Close Menu'; + text = strings.closeMenu; case ActionButtonType.url: icon = null; // Placeholder for URL icon color = ToolkitColors.darkIcon; bgColor = ToolkitColors.transparent; - text = 'Attach Link'; + text = strings.attachFile; textStyle = ToolkitTextStyles.body2; } diff --git a/lib/src/styles/llm_chat_view_style.dart b/lib/src/styles/llm_chat_view_style.dart index ef99b239..14ff42fd 100644 --- a/lib/src/styles/llm_chat_view_style.dart +++ b/lib/src/styles/llm_chat_view_style.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; +import '../strings/strings.dart'; import 'action_button_style.dart'; import 'action_button_type.dart'; import 'chat_input_style.dart'; @@ -46,6 +47,7 @@ class LlmChatViewStyle { this.padding, this.margin, this.messageSpacing, + this.strings, }); /// Resolves the provided [style] with the [defaultStyle]. @@ -81,58 +83,93 @@ class LlmChatViewStyle { ), addButtonStyle: ActionButtonStyle.resolve( style?.addButtonStyle, - defaultStyle: ActionButtonStyle.defaultStyle(ActionButtonType.add), + defaultStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.add, + strings: style?.strings, + ), ), attachFileButtonStyle: ActionButtonStyle.resolve( style?.attachFileButtonStyle, defaultStyle: ActionButtonStyle.defaultStyle( ActionButtonType.attachFile, + strings: style?.strings, ), ), cameraButtonStyle: ActionButtonStyle.resolve( style?.cameraButtonStyle, - defaultStyle: ActionButtonStyle.defaultStyle(ActionButtonType.camera), + defaultStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.camera, + strings: style?.strings, + ), ), stopButtonStyle: ActionButtonStyle.resolve( style?.stopButtonStyle, - defaultStyle: ActionButtonStyle.defaultStyle(ActionButtonType.stop), + defaultStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.stop, + strings: style?.strings, + ), ), closeButtonStyle: ActionButtonStyle.resolve( style?.closeButtonStyle, - defaultStyle: ActionButtonStyle.defaultStyle(ActionButtonType.close), + defaultStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.close, + strings: style?.strings, + ), ), cancelButtonStyle: ActionButtonStyle.resolve( style?.cancelButtonStyle, - defaultStyle: ActionButtonStyle.defaultStyle(ActionButtonType.cancel), + defaultStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.cancel, + strings: style?.strings, + ), ), copyButtonStyle: ActionButtonStyle.resolve( style?.copyButtonStyle, - defaultStyle: ActionButtonStyle.defaultStyle(ActionButtonType.copy), + defaultStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.copy, + strings: style?.strings, + ), ), editButtonStyle: ActionButtonStyle.resolve( style?.editButtonStyle, - defaultStyle: ActionButtonStyle.defaultStyle(ActionButtonType.edit), + defaultStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.edit, + strings: style?.strings, + ), ), galleryButtonStyle: ActionButtonStyle.resolve( style?.galleryButtonStyle, - defaultStyle: ActionButtonStyle.defaultStyle(ActionButtonType.gallery), + defaultStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.gallery, + strings: style?.strings, + ), ), recordButtonStyle: ActionButtonStyle.resolve( style?.recordButtonStyle, - defaultStyle: ActionButtonStyle.defaultStyle(ActionButtonType.record), + defaultStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.record, + strings: style?.strings, + ), ), submitButtonStyle: ActionButtonStyle.resolve( style?.submitButtonStyle, - defaultStyle: ActionButtonStyle.defaultStyle(ActionButtonType.submit), + defaultStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.submit, + strings: style?.strings, + ), ), disabledButtonStyle: ActionButtonStyle.resolve( style?.disabledButtonStyle, - defaultStyle: ActionButtonStyle.defaultStyle(ActionButtonType.disabled), + defaultStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.disabled, + strings: style?.strings, + ), ), closeMenuButtonStyle: ActionButtonStyle.resolve( style?.closeMenuButtonStyle, defaultStyle: ActionButtonStyle.defaultStyle( ActionButtonType.closeMenu, + strings: style?.strings, ), ), actionButtonBarDecoration: @@ -148,7 +185,10 @@ class LlmChatViewStyle { ), urlButtonStyle: ActionButtonStyle.resolve( style?.urlButtonStyle, - defaultStyle: ActionButtonStyle.defaultStyle(ActionButtonType.url), + defaultStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.url, + strings: style?.strings, + ), ), padding: style?.padding ?? defaultStyle.padding, margin: style?.margin ?? defaultStyle.margin, @@ -167,24 +207,53 @@ class LlmChatViewStyle { userMessageStyle: UserMessageStyle.defaultStyle(), llmMessageStyle: LlmMessageStyle.defaultStyle(), chatInputStyle: ChatInputStyle.defaultStyle(), - addButtonStyle: ActionButtonStyle.defaultStyle(ActionButtonType.add), - stopButtonStyle: ActionButtonStyle.defaultStyle(ActionButtonType.stop), - recordButtonStyle: ActionButtonStyle.defaultStyle(ActionButtonType.record), - submitButtonStyle: ActionButtonStyle.defaultStyle(ActionButtonType.submit), + addButtonStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.add, + strings: LlmChatViewStrings.defaults, + ), + stopButtonStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.stop, + strings: LlmChatViewStrings.defaults, + ), + recordButtonStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.record, + strings: LlmChatViewStrings.defaults, + ), + submitButtonStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.submit, + strings: LlmChatViewStrings.defaults, + ), closeMenuButtonStyle: ActionButtonStyle.defaultStyle( ActionButtonType.closeMenu, + strings: LlmChatViewStrings.defaults, ), attachFileButtonStyle: ActionButtonStyle.defaultStyle( ActionButtonType.attachFile, + strings: LlmChatViewStrings.defaults, ), galleryButtonStyle: ActionButtonStyle.defaultStyle( ActionButtonType.gallery, ), - cameraButtonStyle: ActionButtonStyle.defaultStyle(ActionButtonType.camera), - closeButtonStyle: ActionButtonStyle.defaultStyle(ActionButtonType.close), - cancelButtonStyle: ActionButtonStyle.defaultStyle(ActionButtonType.cancel), - copyButtonStyle: ActionButtonStyle.defaultStyle(ActionButtonType.copy), - editButtonStyle: ActionButtonStyle.defaultStyle(ActionButtonType.edit), + cameraButtonStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.camera, + strings: LlmChatViewStrings.defaults, + ), + closeButtonStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.close, + strings: LlmChatViewStrings.defaults, + ), + cancelButtonStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.cancel, + strings: LlmChatViewStrings.defaults, + ), + copyButtonStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.copy, + strings: LlmChatViewStrings.defaults, + ), + editButtonStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.edit, + strings: LlmChatViewStrings.defaults, + ), actionButtonBarDecoration: BoxDecoration( color: ToolkitColors.darkButtonBackground, borderRadius: BorderRadius.circular(20), @@ -192,7 +261,10 @@ class LlmChatViewStyle { fileAttachmentStyle: FileAttachmentStyle.defaultStyle(), suggestionStyle: SuggestionStyle.defaultStyle(), voiceNoteRecorderStyle: VoiceNoteRecorderStyle.defaultStyle(), - urlButtonStyle: ActionButtonStyle.defaultStyle(ActionButtonType.url), + urlButtonStyle: ActionButtonStyle.defaultStyle( + ActionButtonType.url, + strings: LlmChatViewStrings.defaults, + ), ); /// Creates a copy of this style with the given fields replaced by the new @@ -340,4 +412,7 @@ class LlmChatViewStyle { /// Spacing between messages. final double? messageSpacing; + + /// Custom strings for the chat view. + final LlmChatViewStrings? strings; } diff --git a/lib/src/utility.dart b/lib/src/utility.dart index c2caf118..feecfab4 100644 --- a/lib/src/utility.dart +++ b/lib/src/utility.dart @@ -50,10 +50,14 @@ final isMobile = UniversalPlatform.isAndroid || UniversalPlatform.isIOS; /// /// Returns: A [Future] that completes when the text has been copied to the /// clipboard and the confirmation message has been shown. -Future copyToClipboard(BuildContext context, String text) async { +Future copyToClipboard( + BuildContext context, + String text, + String message, +) async { await Clipboard.setData(ClipboardData(text: text)); if (context.mounted) { - AdaptiveSnackBar.show(context, 'Message copied to clipboard'); + AdaptiveSnackBar.show(context, message); } } diff --git a/lib/src/views/chat_input/attachments_action_bar.dart b/lib/src/views/chat_input/attachments_action_bar.dart index 58e76971..594a1ce4 100644 --- a/lib/src/views/chat_input/attachments_action_bar.dart +++ b/lib/src/views/chat_input/attachments_action_bar.dart @@ -8,7 +8,8 @@ import 'package:file_selector/file_selector.dart'; import 'package:flutter/material.dart' show Icons, MenuAnchor, MenuItemButton, MenuStyle; import 'package:flutter/widgets.dart'; -import 'package:flutter_ai_toolkit/src/dialogs/url_input_dialog.dart'; +import '../../dialogs/url_input_dialog.dart'; +import '../../strings/llm_chat_view_strings.dart'; import 'package:flutter_ai_toolkit/src/utility.dart'; import 'package:image_picker/image_picker.dart'; @@ -50,6 +51,7 @@ class _AttachmentActionBarState extends State { Widget build(BuildContext context) => ChatViewModelClient( builder: (context, viewModel, child) { final chatStyle = LlmChatViewStyle.resolve(viewModel.style); + final chatStrings = viewModel.strings; final menuItems = [ if (_canCamera) MenuItemButton( @@ -57,7 +59,7 @@ class _AttachmentActionBarState extends State { chatStyle.cameraButtonStyle!.icon!, color: chatStyle.cameraButtonStyle!.iconColor, ), - onPressed: () => _onCamera(), + onPressed: () => _onCamera(chatStrings), child: Text( chatStyle.cameraButtonStyle!.text!, style: chatStyle.cameraButtonStyle!.textStyle, @@ -68,7 +70,7 @@ class _AttachmentActionBarState extends State { chatStyle.galleryButtonStyle!.icon!, color: chatStyle.galleryButtonStyle!.iconColor, ), - onPressed: () => _onGallery(), + onPressed: () => _onGallery(chatStrings), child: Text( chatStyle.galleryButtonStyle!.text!, style: chatStyle.galleryButtonStyle!.textStyle, @@ -79,7 +81,7 @@ class _AttachmentActionBarState extends State { chatStyle.attachFileButtonStyle!.icon!, color: chatStyle.attachFileButtonStyle!.iconColor, ), - onPressed: () => _onFile(), + onPressed: () => _onFile(chatStrings), child: Text( chatStyle.attachFileButtonStyle!.text!, style: chatStyle.attachFileButtonStyle!.textStyle, @@ -90,7 +92,7 @@ class _AttachmentActionBarState extends State { Icons.link, color: chatStyle.urlButtonStyle!.iconColor, ), - onPressed: () => _onUrl(), + onPressed: () => _onUrl(chatStrings), child: Text( chatStyle.urlButtonStyle!.text!, style: chatStyle.urlButtonStyle!.textStyle, @@ -139,10 +141,15 @@ class _AttachmentActionBarState extends State { return Offset(0, -estimatedMenuHeight); } - void _onCamera() => unawaited(_pickImage(ImageSource.camera)); - void _onGallery() => unawaited(_pickImage(ImageSource.gallery)); + void _onCamera(LlmChatViewStrings chatStrings) => + unawaited(_pickImage(ImageSource.camera, chatStrings)); + void _onGallery(LlmChatViewStrings chatStrings) => + unawaited(_pickImage(ImageSource.gallery, chatStrings)); - Future _pickImage(ImageSource source) async { + Future _pickImage( + ImageSource source, + LlmChatViewStrings chatStrings, + ) async { assert( source == ImageSource.camera || source == ImageSource.gallery, 'Unsupported image source: $source', @@ -165,12 +172,15 @@ class _AttachmentActionBarState extends State { if (context.mounted) { // I just checked this! ^^^ // ignore: use_build_context_synchronously - AdaptiveSnackBar.show(context, 'Unable to pick an image: $ex'); + AdaptiveSnackBar.show( + context, + chatStrings.formatUnableToPickImage(ex.toString()), + ); } } } - Future _onFile() async { + Future _onFile(LlmChatViewStrings chatStrings) async { try { final files = await openFiles(); final attachments = await Future.wait(files.map(FileAttachment.fromFile)); @@ -179,12 +189,15 @@ class _AttachmentActionBarState extends State { if (context.mounted) { // I just checked this! ^^^ // ignore: use_build_context_synchronously - AdaptiveSnackBar.show(context, 'Unable to pick a file: $ex'); + AdaptiveSnackBar.show( + context, + chatStrings.formatUnableToPickFile(ex.toString()), + ); } } } - Future _onUrl() async { + Future _onUrl(LlmChatViewStrings chatStrings) async { try { final url = await showUrlInputDialog(context); if (url == null) return; @@ -193,7 +206,10 @@ class _AttachmentActionBarState extends State { if (context.mounted) { // I just checked this! ^^^ // ignore: use_build_context_synchronously - AdaptiveSnackBar.show(context, 'Unable to pick a URL: $ex'); + AdaptiveSnackBar.show( + context, + chatStrings.formatUnableToPickUrl(ex.toString()), + ); } } } diff --git a/lib/src/views/chat_input/chat_input.dart b/lib/src/views/chat_input/chat_input.dart index 45a886d5..d46b8b50 100644 --- a/lib/src/views/chat_input/chat_input.dart +++ b/lib/src/views/chat_input/chat_input.dart @@ -196,6 +196,7 @@ class _ChatInputState extends State { cancelButtonStyle: _chatStyle!.cancelButtonStyle!, voiceNoteRecorderStyle: _chatStyle!.voiceNoteRecorderStyle!, + chatStrings: _viewModel!.strings, ), ), Padding( @@ -260,7 +261,7 @@ class _ChatInputState extends State { final file = _waveController.file; if (file == null) { - AdaptiveSnackBar.show(context, 'Unable to record audio'); + AdaptiveSnackBar.show(context, _viewModel!.strings.unableToRecordAudio); return; } diff --git a/lib/src/views/chat_input/editing_indicator.dart b/lib/src/views/chat_input/editing_indicator.dart index a9560bdf..85f6518b 100644 --- a/lib/src/views/chat_input/editing_indicator.dart +++ b/lib/src/views/chat_input/editing_indicator.dart @@ -20,6 +20,7 @@ class EditingIndicator extends StatelessWidget { const EditingIndicator({ required this.onCancelEdit, required this.cancelButtonStyle, + required this.editingTitle, super.key, }); @@ -29,6 +30,9 @@ class EditingIndicator extends StatelessWidget { /// The style to be applied to the cancel button. final ActionButtonStyle cancelButtonStyle; + /// The title to be displayed in the editing indicator. + final String editingTitle; + @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.only(right: 16), @@ -38,7 +42,7 @@ class EditingIndicator extends StatelessWidget { spacing: 6, children: [ Text( - 'Editing', + editingTitle, style: ToolkitTextStyles.label.copyWith( color: invertColor(cancelButtonStyle.iconColor), ), diff --git a/lib/src/views/chat_input/text_or_audio_input.dart b/lib/src/views/chat_input/text_or_audio_input.dart index 05732397..72d74002 100644 --- a/lib/src/views/chat_input/text_or_audio_input.dart +++ b/lib/src/views/chat_input/text_or_audio_input.dart @@ -1,5 +1,6 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import '../../strings/llm_chat_view_strings.dart'; import 'package:waveform_recorder/waveform_recorder.dart'; import '../../styles/styles.dart'; @@ -39,6 +40,7 @@ class TextOrAudioInput extends StatelessWidget { required InputState inputState, required ActionButtonStyle cancelButtonStyle, required VoiceNoteRecorderStyle voiceNoteRecorderStyle, + required LlmChatViewStrings chatStrings, }) : _cancelButtonStyle = cancelButtonStyle, _inputState = inputState, _autofocus = autofocus, @@ -49,7 +51,8 @@ class TextOrAudioInput extends StatelessWidget { _onCancelEdit = onCancelEdit, _waveController = waveController, _inputStyle = inputStyle, - _voiceNoteRecorderStyle = voiceNoteRecorderStyle; + _voiceNoteRecorderStyle = voiceNoteRecorderStyle, + _chatStrings = chatStrings; final ChatInputStyle _inputStyle; final WaveformRecorderController _waveController; @@ -62,6 +65,7 @@ class TextOrAudioInput extends StatelessWidget { final InputState _inputState; final ActionButtonStyle _cancelButtonStyle; final VoiceNoteRecorderStyle _voiceNoteRecorderStyle; + final LlmChatViewStrings _chatStrings; static const _minInputHeight = 48.0; static const _maxInputHeight = 144.0; @@ -124,6 +128,7 @@ class TextOrAudioInput extends StatelessWidget { ? EditingIndicator( onCancelEdit: _onCancelEdit, cancelButtonStyle: _cancelButtonStyle, + editingTitle: _chatStrings.editing, ) : const SizedBox(), ), diff --git a/lib/src/views/chat_message_view/adaptive_copy_text.dart b/lib/src/views/chat_message_view/adaptive_copy_text.dart index 40b47403..b859bc22 100644 --- a/lib/src/views/chat_message_view/adaptive_copy_text.dart +++ b/lib/src/views/chat_message_view/adaptive_copy_text.dart @@ -7,9 +7,9 @@ import 'package:flutter/material.dart' SelectionArea, DefaultWidgetsLocalizations; import 'package:flutter/widgets.dart'; +import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; import 'package:flutter_context_menu/flutter_context_menu.dart'; -import '../../styles/llm_chat_view_style.dart'; import '../../utility.dart'; /// A widget that displays text with adaptive copy functionality. @@ -30,6 +30,7 @@ class AdaptiveCopyText extends StatelessWidget { required this.clipboardText, required this.child, required this.chatStyle, + required this.chatStrings, this.onEdit, super.key, }); @@ -46,20 +47,30 @@ class AdaptiveCopyText extends StatelessWidget { /// The style information for the chat. final LlmChatViewStyle chatStyle; + /// The strings used for text in the chat interface. + final LlmChatViewStrings chatStrings; + @override Widget build(BuildContext context) { final contextMenu = ContextMenu( entries: [ if (onEdit != null) MenuItem( - label: const Text('Edit'), + label: Text(chatStrings.edit), icon: Icon(chatStyle.editButtonStyle!.icon), onSelected: (_) => onEdit?.call(), ), MenuItem( - label: const Text('Copy'), + label: Text(chatStrings.copy), icon: Icon(chatStyle.copyButtonStyle!.icon), - onSelected: (_) => unawaited(copyToClipboard(context, clipboardText)), + onSelected: + (_) => unawaited( + copyToClipboard( + context, + clipboardText, + chatStrings.copyToClipboard, + ), + ), ), ], ); diff --git a/lib/src/views/chat_message_view/hovering_buttons.dart b/lib/src/views/chat_message_view/hovering_buttons.dart index 73f60a95..cb37b1e0 100644 --- a/lib/src/views/chat_message_view/hovering_buttons.dart +++ b/lib/src/views/chat_message_view/hovering_buttons.dart @@ -20,6 +20,7 @@ class HoveringButtons extends StatelessWidget { required this.isUserMessage, required this.child, this.clipboardText, + required this.clipboardMessage, this.onEdit, super.key, }); @@ -33,6 +34,9 @@ class HoveringButtons extends StatelessWidget { /// The text to be copied to the clipboard. final String? clipboardText; + ///The text to be shown when copying to the clipboard. + final String clipboardMessage; + /// The child widget over which the buttons will hover. final Widget child; @@ -86,6 +90,7 @@ class HoveringButtons extends StatelessWidget { copyToClipboard( context, clipboardText!, + clipboardMessage, ), ), child: Icon( diff --git a/lib/src/views/chat_message_view/llm_message_view.dart b/lib/src/views/chat_message_view/llm_message_view.dart index 15801552..abbc57e1 100644 --- a/lib/src/views/chat_message_view/llm_message_view.dart +++ b/lib/src/views/chat_message_view/llm_message_view.dart @@ -42,6 +42,7 @@ class LlmMessageView extends StatelessWidget { final text = message.text; final chatStyle = LlmChatViewStyle.resolve(viewModel.style); final llmStyle = LlmMessageStyle.resolve(chatStyle.llmMessageStyle); + final chatString = viewModel.strings; return Flexible( flex: llmStyle.flex, @@ -70,6 +71,7 @@ class LlmMessageView extends StatelessWidget { isUserMessage: false, chatStyle: chatStyle, clipboardText: text, + clipboardMessage: chatString.copyToClipboard, child: Container( width: double.infinity, decoration: llmStyle.decoration, @@ -87,6 +89,7 @@ class LlmMessageView extends StatelessWidget { : AdaptiveCopyText( clipboardText: text, chatStyle: chatStyle, + chatStrings: chatString, child: isWelcomeMessage || viewModel.responseBuilder == null diff --git a/lib/src/views/chat_message_view/user_message_view.dart b/lib/src/views/chat_message_view/user_message_view.dart index 56c55ea6..91e3e4fb 100644 --- a/lib/src/views/chat_message_view/user_message_view.dart +++ b/lib/src/views/chat_message_view/user_message_view.dart @@ -55,6 +55,7 @@ class UserMessageView extends StatelessWidget { final userStyle = UserMessageStyle.resolve( chatStyle.userMessageStyle, ); + final chatStrings = viewModel.strings; return Align( alignment: Alignment.topRight, @@ -64,6 +65,7 @@ class UserMessageView extends StatelessWidget { isUserMessage: true, chatStyle: chatStyle, clipboardText: text, + clipboardMessage: chatStrings.copyToClipboard, onEdit: onEdit, child: DecoratedBox( decoration: userStyle.decoration!, @@ -77,6 +79,7 @@ class UserMessageView extends StatelessWidget { child: AdaptiveCopyText( chatStyle: chatStyle, clipboardText: text, + chatStrings: chatStrings, onEdit: onEdit, child: Text(text, style: userStyle.textStyle), ), diff --git a/lib/src/views/llm_chat_view/llm_chat_view.dart b/lib/src/views/llm_chat_view/llm_chat_view.dart index 72c9084d..1b074436 100644 --- a/lib/src/views/llm_chat_view/llm_chat_view.dart +++ b/lib/src/views/llm_chat_view/llm_chat_view.dart @@ -16,6 +16,7 @@ import '../../platform_helper/platform_helper.dart' as ph; import '../../providers/interface/attachments.dart'; import '../../providers/interface/chat_message.dart'; import '../../providers/interface/llm_provider.dart'; +import '../../strings/llm_chat_view_strings.dart'; import '../../styles/llm_chat_view_style.dart'; import '../chat_history_view.dart'; import '../chat_input/chat_input.dart'; @@ -76,6 +77,8 @@ class LlmChatView extends StatefulWidget { /// during a chat operation. Defaults to 'ERROR'. /// - [enableAttachments]: Optional. Whether to enable file and image attachments in the chat input. /// - [enableVoiceNotes]: Optional. Whether to enable voice notes in the chat input. + /// - [strings]: Optional. Custom strings for the chat interface. If not provided, + /// the default strings will be used. LlmChatView({ required LlmProvider provider, LlmChatViewStyle? style, @@ -91,6 +94,7 @@ class LlmChatView extends StatefulWidget { this.enableAttachments = true, this.enableVoiceNotes = true, this.autofocus, + LlmChatViewStrings? strings, super.key, }) : viewModel = ChatViewModel( provider: provider, @@ -102,8 +106,15 @@ class LlmChatView extends StatefulWidget { welcomeMessage: welcomeMessage, enableAttachments: enableAttachments, enableVoiceNotes: enableVoiceNotes, + strings: strings ?? LlmChatViewStrings.defaults, ); + /// The strings used throughout the chat interface. + /// + /// This provides access to all the text strings used in the chat interface, + /// allowing for easy customization and internationalization. + LlmChatViewStrings get strings => viewModel.strings; + /// Whether to enable file and image attachments in the chat input. /// /// When set to false, the attachment button and related functionality will be