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