diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 68984b6..81e9ad5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,10 @@ + + + + diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 7bb2df6..3c85cfe 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/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.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index b9e43bd..3756b18 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.1.0" apply false - id "org.jetbrains.kotlin.android" version "1.8.22" apply false + id "com.android.application" version "8.5.0" apply false + id "org.jetbrains.kotlin.android" version "1.9.0" apply false } include ":app" diff --git a/assets/images/botImage.jpg b/assets/images/botImage.jpg new file mode 100644 index 0000000..f2a8734 Binary files /dev/null and b/assets/images/botImage.jpg differ diff --git a/lib/controllers/home_controller.dart b/lib/controllers/home_controller.dart deleted file mode 100644 index 05415b0..0000000 --- a/lib/controllers/home_controller.dart +++ /dev/null @@ -1,268 +0,0 @@ -import 'package:flutter_tts/flutter_tts.dart'; -import 'package:get/get.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:speech_to_text/speech_to_text.dart' as stt; -import 'package:voice_assistant/repository/network_requests.dart'; -import 'package:voice_assistant/utils/alert_messages.dart'; - -import '../exceptions/app_exception.dart'; -import '../models/chat_model.dart'; - -class HomeController extends GetxController { - // External services and plugins - final stt.SpeechToText speech = stt.SpeechToText(); // Handles speech-to-text functionality - final flutterTts = FlutterTts(); // Handles text-to-speech functionality - final _service = NetworkRequests(); // Makes API requests to custom network services - - // Observable variables for UI updates - final RxString greetingMessage = "Good Morning".obs; // Stores the greeting message for display - final RxString _userVoiceMsg = "".obs; // Stores the recognized user voice message from speech-to-text - final RxBool _speechEnabled = false.obs; // Flag to track if speech recognition is enabled - final RxBool speechListen = false.obs; // Flag to indicate if app is actively listening to user speech - final RxBool textResponse = false.obs; // Flag to indicate if a text response is received - final RxBool isLoading = false.obs; // Flag to show loading state when waiting for API response - final RxBool isStopped = true.obs; // Flag to determine if text-to-speech should stop - final RxList messages = [].obs; // List to hold conversation messages - - List messageQueue = []; // Queue for storing messages to be spoken by text-to-speech - - @override - void onInit() { - super.onInit(); - initialize(); // Initialize the controller by setting greeting and TTS configurations - } - - @override - void onClose() { - stopTTs(); // Stops any active text-to-speech - stopListening(); // Stops any active speech-to-text - } - - Future askPermission() async { // Asks for microphone permission and opens settings if denied - var requestStatus = await Permission.microphone.request(); - if (requestStatus.isDenied || requestStatus.isPermanentlyDenied) { - await openAppSettings(); - } else if (requestStatus.isGranted) { - speechInitialize(); // Initializes speech recognition if permission is granted - } - } - - // Calls an API to get a response from the Gemini model, adding it to messages and speaking it - Future callGeminiAPI() async { - try { - final data = await _service.geminiAPI(messages: messages); - if (data != null && data.candidates != null) { - final botResponse = data.candidates!.first.content.parts.first.text; - messages.add(Contents(role: "model", parts: [Parts(text: botResponse)], isImage: false)); - speakTTs(botResponse); - } else { - messages.add(Contents( - role: "model", parts: [ - Parts(text: "Sorry, I am not able to gives you a response of your prompt") - ], isImage: false)); - speakTTs("Sorry, I am not able to gives you a response of your prompt"); - } - } on AppException catch (e) { - AlertMessages.showSnackBar(e.message.toString()); - } catch (e) { // Handles errors, showing an error message - AlertMessages.showSnackBar(e.toString()); - } finally { - isLoading.value = false; // Ends loading state - } - } - - // Calls the Imagine API to fetch an image based on input text - Future callImagineAPI(String input) async { - try { - final data = await _service.imagineAPI(input); - messages.add(Contents( - role: "user", parts: [ - Parts(text: "Here, is a comprehensive desire image output of your prompt"), - ], isImage: false)); - messages.add(Contents(role: "model", parts: [Parts(text: data)], isImage: true)); - } on AppException catch (e) { // Adds an error message if the call fails - messages.add(Contents(role: "model", parts: [Parts(text: "Failed")], isImage: false)); - AlertMessages.showSnackBar(e.message.toString()); - } catch (e) { // Adds an error message if the call fails - messages.add(Contents(role: "model", parts: [Parts(text: "Failed")], isImage: false)); - AlertMessages.showSnackBar(e.toString()); - } finally { - isLoading.value = false; - } - } - - // Shows a bottom sheet message when there's an error with audio permissions - Future audioPermission(String error) async { - await Get.bottomSheet( - elevation: 8.0, - ignoreSafeArea: true, - persistent: true, - isDismissible: false, - enableDrag: false, - AlertMessages.bottomSheet(msg: "Error: $error")); - } - - // Initializes greeting message, speech recognition, and TTS settings - Future initialize() async { - greetingMessage.value = getGreeting(); // Sets initial greeting based on time of day - await speechInitialize(); - await flutterTts.setLanguage("en-US"); // Sets TTS language - await flutterTts.setSpeechRate(0.5); // Sets TTS speaking speed - await flutterTts.setVolume(1.0); // Sets TTS volume - await flutterTts.setPitch(1.0); // Sets TTS pitch - flutterTts.setCompletionHandler(_onSpeakCompleted); // Sets a handler for TTS completion - } - - // Returns a greeting based on the time of day - String getGreeting() { - final hour = DateTime.now().hour; - if (hour < 12) { - return "Good Morning"; - } else if (hour < 17) { - return "Good Afternoon"; - } else { - return "Good Evening"; - } - } - - // Callback to handle changes in the speech recognition status - void _onSpeechStatus(String status) async { - if (status == "notListening") { - speechListen.value = false; // Updates flag when user stops speaking - await Future.delayed(const Duration(seconds: 2)); // here delay is used to store the speech words, if not it will miss the last word of your prompt - _sendRequest(_userVoiceMsg.value); // Process the captured input - stopListening(); // Stops speech recognition - } - } - - // Called when TTS completes a message, then checks for more messages in queue - void _onSpeakCompleted() { - if (!isStopped.value) { - _speakNextMessage(); // Speak the next message if not stopped - } - } - - // Initializes the message queue for speaking and starts TTS - void playTTs() async { - for (var message in messages) { - if (!message.isImage) messageQueue.add(message.parts.first.text); - } - isStopped.value = false; - flutterTts.setCompletionHandler(_onSpeakCompleted); - await _speakNextMessage(); // Begins speaking messages in the queue - } - - // Resets conversation messages and stops both TTS and speech recognition - void resetAll() { - messages.clear(); - messageQueue.clear(); - textResponse.value = false; - stopTTs(); - stopListening(); - } - - // Sends user input to appropriate API and speaks the response if needed - Future _sendRequest(String input) async { - try { - textResponse.value = true; - if (input.isNotEmpty) { - messages.add(Contents(role: "user", parts: [Parts(text: input)], isImage: false)); - isLoading.value = true; - final response = await _service.isArtPromptAPI(input); - if (input.contains("draw") || - input.contains("image") || - input.contains("picture")) { - await callImagineAPI(input); // Calls Imagine API if input asks for an image - } else if (response == "NO") { - await callGeminiAPI(); // Calls Gemini API if text response is expected - } else if (response == "YES") { - await callImagineAPI(input); // Calls Imagine API if input asks for an image - } else { - isLoading.value = false; - messages.add(Contents(role: "model", parts: [Parts(text: response)], isImage: false)); - speakTTs(response); // Speaks response if applicable - } - } else { - // Adds a default prompt message when no input is provided - messages.add(Contents( - role: "user", parts: [ - Parts(text: "Please provide me with some context or a question so I can assist you.") - ], isImage: false)); - messageQueue.add("Please provide me with some context or a question so I can assist you."); - messages.add(Contents( - role: "model", parts: [ - Parts(text: "For example: Give me some Interview Tips.") - ], isImage: false)); - messageQueue.add("For example: Give me some Interview Tips."); - isStopped.value = false; - await _speakNextMessage(); - } - } on AppException catch (e) { - isLoading.value = false; - messages.add(Contents( - role: "model", parts: [Parts(text: "Failed")], isImage: false)); - AlertMessages.showSnackBar(e.message.toString()); - } catch (e) { - isLoading.value = false; - messages.add(Contents(role: "model", parts: [Parts(text: "Failed")], isImage: false)); - AlertMessages.showSnackBar(e.toString()); - } - } - - // Initializes speech recognition and sets error handlers - Future speechInitialize() async { - _speechEnabled.value = await speech.initialize( - onStatus: (status) => _onSpeechStatus(status), // Sets status change handler - onError: (error) => AlertMessages.showSnackBar(error.errorMsg) // Shows error on initialization failure - ); - if (!_speechEnabled.value) { - audioPermission("Speech recognition is not available on this device."); - } - } - - // Speaks the next message in the queue if available - Future _speakNextMessage() async { - if (messageQueue.isNotEmpty && !isStopped.value) { - await flutterTts.speak(messageQueue.removeAt(0)); // Speaks the next message in queue - } else { - isStopped.value = true; // Sets stopped flag when queue is empty - } - } - - // Adds a message to the queue and starts TTS - Future speakTTs(String botResponse) async { - isStopped.value = false; - messageQueue.add(botResponse); - await _speakNextMessage(); - } - - // Adds a message to the queue and starts TTS - Future stopTTs() async { - isStopped.value = true; - await flutterTts.stop(); - } - - // Each time to start a speech recognition session - Future startListening() async { - speechListen.value = true; - await speech.listen( - onResult: (result) { - _userVoiceMsg.value = result.recognizedWords; // Captures user's speech as text - }, - listenOptions: stt.SpeechListenOptions( - partialResults: true, - listenMode: stt.ListenMode - .dictation, // Use dictation mode for continuous listening - cancelOnError: true), - pauseFor: const Duration(seconds: 2), - ); - } - - /// Manually stop the active speech recognition session Note that there are also timeouts that each platform enforces - /// and the SpeechToText plugin supports setting timeouts on the listen method. - Future stopListening() async { - _userVoiceMsg.value = ""; - speechListen.value = false; - await speech.stop(); - } -} diff --git a/lib/data/adapters/models_adapter.dart b/lib/data/adapters/models_adapter.dart new file mode 100644 index 0000000..f1d7fbb --- /dev/null +++ b/lib/data/adapters/models_adapter.dart @@ -0,0 +1,38 @@ +import 'package:hive/hive.dart'; + +part 'models_adapter.g.dart'; + +@HiveType(typeId: 0) +class HiveChatBox extends HiveObject { + @HiveField(0) + String id; + + @HiveField(1) + String title; + + @HiveField(2) + List messages; + + HiveChatBox({required this.id, required this.title, required this.messages}); +} + +@HiveType(typeId: 1) +class HiveChatBoxMessages extends HiveObject { + @HiveField(0) + String text; + + @HiveField(1) + bool isUser; + + @HiveField(2) + List? imagePath; + + @HiveField(3) + String? filePath; + + HiveChatBoxMessages( + {required this.text, + required this.isUser, + this.imagePath, + this.filePath}); +} diff --git a/lib/data/adapters/models_adapter.g.dart b/lib/data/adapters/models_adapter.g.dart new file mode 100644 index 0000000..62a1389 --- /dev/null +++ b/lib/data/adapters/models_adapter.g.dart @@ -0,0 +1,90 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'models_adapter.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class HiveChatBoxAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + HiveChatBox read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return HiveChatBox( + id: fields[0] as String, + title: fields[1] as String, + messages: (fields[2] as List).cast(), + ); + } + + @override + void write(BinaryWriter writer, HiveChatBox obj) { + writer + ..writeByte(3) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.title) + ..writeByte(2) + ..write(obj.messages); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is HiveChatBoxAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class HiveChatBoxMessagesAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + HiveChatBoxMessages read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return HiveChatBoxMessages( + text: fields[0] as String, + isUser: fields[1] as bool, + imagePath: (fields[2] as List?)?.cast(), + filePath: fields[3] as String?, + ); + } + + @override + void write(BinaryWriter writer, HiveChatBoxMessages obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.text) + ..writeByte(1) + ..write(obj.isUser) + ..writeByte(2) + ..write(obj.imagePath) + ..writeByte(3) + ..write(obj.filePath); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is HiveChatBoxMessagesAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/hivedata/chat_data.dart b/lib/data/hivedata/chat_data.dart new file mode 100644 index 0000000..269db7f --- /dev/null +++ b/lib/data/hivedata/chat_data.dart @@ -0,0 +1,49 @@ +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; + +import '../adapters/models_adapter.dart'; + +class ChatData { + final Box chatBox; + + ChatData({required this.chatBox}); + // Fetches chat history for a specific box ID + List? getChatHistory(String boxId) { + final HiveChatBox? chat = chatBox.get(boxId); + return chat?.messages; + } + + // Saves a new message to the chat box + void saveMessage( + String boxId, String boxTitle, RxList messages) { + final chat = chatBox.get(boxId); + if (chat != null) { + chat.messages = messages; + chat.save(); + } else { + // Create a new chat box if it doesn't exist + final newChat = + HiveChatBox(id: boxId, messages: messages, title: boxTitle); + chatBox.put(boxId, newChat); + } + } + + // Retrieves all chat boxes + List getAllChatBoxes() { + return chatBox.values.toList(); + } + + HiveChatBox getLastChatBox() { + final list = getAllChatBoxes(); + return list.last; + } + + Future deleteChatBox({required String chatId}) async { + bool b = await chatBox.delete(chatId).then((value) { + return true; + }).catchError((error) { + return false; + }); + return b; + } +} diff --git a/lib/models/chat_response_model.dart b/lib/data/models/chat_response_model.dart similarity index 69% rename from lib/models/chat_response_model.dart rename to lib/data/models/chat_response_model.dart index e211408..13a6d0e 100644 --- a/lib/models/chat_response_model.dart +++ b/lib/data/models/chat_response_model.dart @@ -7,32 +7,32 @@ class ChatResponseModel { return ChatResponseModel( candidates: json['candidates'] != null ? (json['candidates'] as List) - .map((e) => Candidate.fromJson(e)) - .toList() + .map((e) => Candidate.fromJson(e)) + .toList() : null, ); } } class Candidate { - final Content content; + final ResponseContent content; Candidate({required this.content}); factory Candidate.fromJson(Map json) { return Candidate( - content: Content.fromJson(json['content']), + content: ResponseContent.fromJson(json['content']), ); } } -class Content { +class ResponseContent { final List parts; - Content({required this.parts}); + ResponseContent({required this.parts}); - factory Content.fromJson(Map json) { - return Content( + factory ResponseContent.fromJson(Map json) { + return ResponseContent( parts: (json['parts'] as List).map((e) => Part.fromJson(e)).toList(), ); } diff --git a/lib/data/models/prompt_model.dart b/lib/data/models/prompt_model.dart new file mode 100644 index 0000000..2b2bc01 --- /dev/null +++ b/lib/data/models/prompt_model.dart @@ -0,0 +1,49 @@ +import 'package:flutter_gemini/flutter_gemini.dart'; + +class ChatBoxModel { + final String id; + final String title; + final List messages; + + ChatBoxModel({required this.id, required this.title, required this.messages}); + + Map toJson() => { + 'id': id, + 'title': title, + 'messages': messages.map((msg) => msg.toJson()).toList(), + }; + + factory ChatBoxModel.fromJson(Map json) => ChatBoxModel( + id: json['id'], + title: json['title'], + messages: (json['messages'] as List) + .map((msg) => PromptModel.fromJson(msg)) + .toList(), + ); +} + +class PromptModel { + final TextPart part; + final bool isUser; + final String? imagePath; + final String? filePath; + + PromptModel( + {required this.part, + required this.isUser, + this.imagePath, + this.filePath}); + + Map toJson() => { + 'text': part, + 'isUser': isUser, + 'imagePath': imagePath, + 'filePath': filePath + }; + + factory PromptModel.fromJson(Map json) => PromptModel( + part: json['text'], + isUser: json['isUser'], + imagePath: json['imagePath'], + filePath: json['filePath']); +} diff --git a/lib/exceptions/app_exception.dart b/lib/domain/exceptions/app_exception.dart similarity index 90% rename from lib/exceptions/app_exception.dart rename to lib/domain/exceptions/app_exception.dart index d0dccfe..10e4297 100644 --- a/lib/exceptions/app_exception.dart +++ b/lib/domain/exceptions/app_exception.dart @@ -1,4 +1,3 @@ - class AppException implements Exception { final String? message; final ExceptionType? type; @@ -9,11 +8,11 @@ class AppException implements Exception { }); } -enum ExceptionType{ +enum ExceptionType { internet, format, http, api, timeout, other, -} \ No newline at end of file +} diff --git a/lib/repository/network_requests.dart b/lib/domain/repository/network_requests.dart similarity index 56% rename from lib/repository/network_requests.dart rename to lib/domain/repository/network_requests.dart index 515c3e4..6209da0 100644 --- a/lib/repository/network_requests.dart +++ b/lib/domain/repository/network_requests.dart @@ -4,22 +4,23 @@ import 'dart:io'; import 'package:http/http.dart' as http; +import '../../data/models/chat_response_model.dart'; +import '../../utils/config.dart'; import '../exceptions/app_exception.dart'; -import '../models/chat_model.dart'; -import '../models/chat_response_model.dart'; class NetworkRequests { - final List> messages = []; - final String _geminiAIKey = "Gemini Key"; - final String _apiUrl = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key='; - final String _imageGeneratorKey = "Imagine API Key"; + final String _geminiAIKey = Config.geminiKey; + final String _contentUrl = Config.geminiContentUrl; + final String _imagineUrl = Config.imagineUrl; + final String _imagineKey = Config.imagineKey; Future isArtPromptAPI(String prompt) async { try { - final lastPrompt = "Does this prompt want to generate or drawn an AI image, photo, art, picture, drawing or scenery something related?. then, simply answer with a YES or No. here is a prompt: $prompt."; + final lastPrompt = + "Does this prompt want to generate or drawn an AI image, photo, art, picture, drawing or scenery something related?. then, simply answer with a YES or No. here is a prompt: $prompt."; final res = await http.post( - Uri.parse("$_apiUrl$_geminiAIKey"), + Uri.parse("$_contentUrl$_geminiAIKey"), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'contents': [ @@ -42,54 +43,13 @@ class NetworkRequests { if (res.statusCode == 200) { final response = ChatResponseModel.fromJson(jsonDecode(res.body)); - if (response.candidates!.first.content.parts.first.text == "YES") { + if (response.candidates!.first.content.parts.first.text == "YES\n") { return "YES"; } else { return "NO"; } } - return 'An internal error occurred'; - } on SocketException { - throw AppException( - message: 'No Internet connection', type: ExceptionType.internet); - } on HttpException { - throw AppException( - message: "Couldn't find the data", type: ExceptionType.http); - } on FormatException { - throw AppException( - message: "Bad response format", type: ExceptionType.format); - } on TimeoutException catch (_) { - throw AppException( - message: 'Connection timed out', - type: ExceptionType.timeout, - ); - } - } - - Future geminiAPI( - {required List messages}) async { - try { - final res = await http.post( - Uri.parse("$_apiUrl$_geminiAIKey"), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - 'contents': messages.map((e) => e.toJson()).toList(), - 'generationConfig': { - 'temperature': 0.1, - 'topK': 40, - 'topP': 1, - 'maxOutputTokens': 2048, - 'responseMimeType': 'text/plain', - }, - }), - ); - - if (res.statusCode == 200) { - final responseData = jsonDecode(res.body); - return ChatResponseModel.fromJson(responseData); - } else { - return null; - } + throw 'An internal error occurred'; } on SocketException { throw AppException( message: 'No Internet connection', type: ExceptionType.internet); @@ -108,23 +68,22 @@ class NetworkRequests { } Future imagineAPI(String prompt) async { - final url = Uri.parse('https://api.vyro.ai/v1/imagine/api/generations'); + final url = Uri.parse(_imagineUrl); try { final fields = { 'prompt': prompt, - 'style_id': '122', + 'style': 'realistic', 'aspect_ratio': "3:4", - 'high_res_results': '1', - 'cfg': '7', - 'samples': '1', + 'seed': "2" // Add more parameters as needed for safety and quality }; final multipartRequest = http.MultipartRequest('POST', url); - multipartRequest.headers['Authorization'] = 'Bearer $_imageGeneratorKey'; + multipartRequest.headers['Authorization'] = 'Bearer $_imagineKey'; multipartRequest.fields.addAll(fields); - http.Response response = await http.Response.fromStream(await multipartRequest.send()); + http.Response response = + await http.Response.fromStream(await multipartRequest.send()); if (response.statusCode == 200) { final imageBytes = response.bodyBytes; @@ -134,11 +93,13 @@ class NetworkRequests { return tempFile.path; } else if (response.statusCode == 500) { throw AppException( - message: 'Internal Server Error: Retry the request or contact support', + message: + 'Internal Server Error: Retry the request or contact support', type: ExceptionType.api); } else if (response.statusCode == 503) { throw AppException( - message: 'Service Unavailable: The service is currently unavailable. Retry the request later', + message: + 'Service Unavailable: The service is currently unavailable. Retry the request later', type: ExceptionType.api); } else { throw AppException( @@ -161,4 +122,5 @@ class NetworkRequests { ); } } + } diff --git a/lib/domain/usecases/generate_content.dart b/lib/domain/usecases/generate_content.dart new file mode 100644 index 0000000..4d40a1f --- /dev/null +++ b/lib/domain/usecases/generate_content.dart @@ -0,0 +1,83 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:google_generative_ai/google_generative_ai.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:pdf_gemini/pdf_gemini.dart'; + +import '../../utils/config.dart'; + +class GenerateContentUseCase { + late GenerativeModel generativeModel; + final genService = GenaiClient(geminiApiKey: Config.geminiKey); + + GenerateContentUseCase() { + generativeModel = GenerativeModel( + model: 'gemini-1.5-flash', + apiKey: Config.geminiKey, + generationConfig: GenerationConfig( + temperature: 0.4, + topK: 32, + topP: 1, + maxOutputTokens: 4096, + ), + safetySettings: [ + SafetySetting(HarmCategory.harassment, HarmBlockThreshold.high), + SafetySetting(HarmCategory.hateSpeech, HarmBlockThreshold.high), + ]); + } + + // Sends a prompt to the generative model and returns the response + Future> execute( + {required String prompt, + required List? history, + required bool isTextOnly, + required List images}) async { + try { + final content = await getContent( + message: prompt, isTextOnly: isTextOnly, images: images); + final chat = generativeModel.startChat(history: history); + return chat.sendMessageStream(content).asyncMap((event) { + return event; + }); + } catch (e) { + throw Exception("Error generating content: $e"); + } + } + + Future getContent( + {required String message, + required bool isTextOnly, + required List images}) async { + if (isTextOnly) { + // generate text from text-only input + return Content.text(message); + } else { + // generate image from text and image input + final imageFutures = images + .map((imageFile) => imageFile.readAsBytes()) + .toList(growable: false); + + final imageBytes = await Future.wait(imageFutures); + final prompt = TextPart(message); + final imageParts = imageBytes + .map((bytes) => DataPart('image/jpeg', Uint8List.fromList(bytes))) + .toList(); + + return Content.multi([...imageParts, prompt]); + } + } + + Future sendPromptFile( + {required String prompt, + required File file, + required String fileName}) async { + final data = await genService.promptDocument( + fileName, + 'pdf', + file.readAsBytesSync(), + prompt, + ); + return data.text; + } +} diff --git a/lib/domain/usecases/title_generator.dart b/lib/domain/usecases/title_generator.dart new file mode 100644 index 0000000..cb00d75 --- /dev/null +++ b/lib/domain/usecases/title_generator.dart @@ -0,0 +1,83 @@ +class ChatTitleGenerator { + // Function to generate a dynamic title based on content + static String generateTitle(String userPrompt, String aiResponse) { + // Combine user prompt and response for analysis + String combinedText = "$userPrompt $aiResponse"; + + // Attempt to extract a meaningful phrase or word (basic NLP simulation) + String title = _extractKeywords(combinedText); + + // If no meaningful keywords are extracted, fallback to a summary + if (title.isEmpty) { + title = _summarizeText(userPrompt, aiResponse); + } + + return title.isNotEmpty + ? _sanitizeTitle(capitalizeTitle(title)) + : "Voice Genie Prompt"; + } + + // Extract potential keywords or meaningful phrases + static String _extractKeywords(String text) { + // Split text into words + List words = text.split(' '); + List keywords = specializedKeywords(words); + + // Return the most relevant keyword or a combination of the top 5 + if (keywords.isNotEmpty) { + return (keywords.length < 3) + ? keywords.take(keywords.length).join(' ') + : keywords.take(3).join(' '); + } + return ''; + } + + // A fallback summarization function + static String _summarizeText(String userPrompt, String aiResponse) { + String baseText = + (userPrompt.length > aiResponse.length) ? userPrompt : aiResponse; + return _extractKeywords(baseText); + } + + static List specializedKeywords(List words) { + // Filter for nouns/proper nouns or special words (basic logic: use longer words, but no common stop words) + const stopWords = [ + 'because', + 'however', + 'during', + ]; + return words.where((word) { + word = word.toLowerCase().trim(); + return word.length >= 6 && !stopWords.contains(word); + }).toList(); + } + + static String capitalizeTitle(String input) { + List words = input.split(' '); + List capitalizedWords = words.map((word) { + return '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}'; + }).toList(); + return capitalizedWords.join(' '); + } + + static String _sanitizeTitle(String title) { + // Define special keywords or characters to remove + const specialKeywords = [ + // Add any special words you want to exclude + '!', '@', '#', '%', '^', '&', '*', '**', '?', + ]; + + // Remove newlines, trailing spaces, and special keywords + title = title + .replaceAll('\n', ' ') + .trim(); // Remove newlines and trailing spaces + for (String keyword in specialKeywords) { + title = title.replaceAll(keyword, ''); + } + + // Normalize multiple spaces to a single space + title = title.replaceAll(RegExp(r'\s+'), ' ').trim(); + + return title; + } +} diff --git a/lib/main.dart b/lib/main.dart index aeab5d5..b6b2db4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,22 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:voice_assistant/screens/home_screen.dart'; +import 'package:hive_flutter/adapters.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:voice_assistant/presentation/views/home_screen.dart'; -void main() { +import 'data/adapters/models_adapter.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + final appDocumentDir = await getApplicationDocumentsDirectory(); + Hive.init(appDocumentDir.path); + // Initialize Hive + await Hive.initFlutter(); + // Register the adapter + Hive.registerAdapter(HiveChatBoxAdapter()); + Hive.registerAdapter(HiveChatBoxMessagesAdapter()); + // Open a box (database) + await Hive.openBox('ChatBoxesHistories'); runApp(const MyApp()); } @@ -16,9 +30,13 @@ class MyApp extends StatelessWidget { debugShowCheckedModeBanner: false, title: 'Voice Genie', theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.tealAccent.shade400), + colorScheme: + ColorScheme.fromSeed(seedColor: Colors.tealAccent.shade400), primaryColor: Colors.tealAccent.shade400, - appBarTheme: AppBarTheme(color: Colors.tealAccent.shade400, centerTitle: true, titleTextStyle: const TextStyle(color: Colors.white, fontSize: 18)), + appBarTheme: AppBarTheme( + color: Colors.tealAccent.shade400, + centerTitle: true, + titleTextStyle: const TextStyle(color: Colors.white, fontSize: 18)), scaffoldBackgroundColor: Colors.grey.shade200, useMaterial3: true, ), diff --git a/lib/models/chat_model.dart b/lib/models/chat_model.dart deleted file mode 100644 index c676dc5..0000000 --- a/lib/models/chat_model.dart +++ /dev/null @@ -1,38 +0,0 @@ -class ChatRequestModel { - final List contents; - - ChatRequestModel({required this.contents}); - - Map toJson() { - return { - 'contents': contents.map((content) => content.toJson()).toList(), - }; - } -} - -class Contents { - final String role; - final List parts; - final bool isImage; - - Contents({required this.role, required this.parts, this.isImage = false,}); - - Map toJson() { - return { - 'role': role, - 'parts': parts.map((part) => part.toJson()).toList(), - }; - } -} - -class Parts { - final String text; - - Parts({required this.text}); - - Map toJson() { - return { - 'text': text, - }; - } -} diff --git a/lib/presentation/controllers/home_controller.dart b/lib/presentation/controllers/home_controller.dart new file mode 100644 index 0000000..800d07f --- /dev/null +++ b/lib/presentation/controllers/home_controller.dart @@ -0,0 +1,541 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_tts/flutter_tts.dart'; +import 'package:get/get.dart'; +import 'package:google_generative_ai/google_generative_ai.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:speech_to_text/speech_to_text.dart' as stt; +import 'package:voice_assistant/domain/usecases/generate_content.dart'; +import 'package:voice_assistant/domain/usecases/title_generator.dart'; +import 'package:voice_assistant/utils/alert_messages.dart'; + +import '../../data/adapters/models_adapter.dart'; +import '../../data/hivedata/chat_data.dart'; +import '../../domain/exceptions/app_exception.dart'; +import '../../domain/repository/network_requests.dart'; + +class HomeController extends GetxController { + // Dependencies + final stt.SpeechToText speech = + stt.SpeechToText(); // Handles speech-to-text functionality + final FlutterTts _flutterTts = + FlutterTts(); // Handles text-to-speech functionality + final NetworkRequests _service = + NetworkRequests(); // Makes API requests to custom network services + final ChatData _chatData; + final GenerateContentUseCase _generateContent; + + // Observable variables for UI updates + final RxInt currentIndex = + 0.obs; // to update the index of pages + final RxInt initialAllChatBoxes = + 0.obs; // indicate how many chat boxes already created whenever the app start + final RxString greetingMessage = + "".obs; // Stores the greeting message for display + final RxString _userVoiceMsg = + "".obs; // Stores the recognized user voice message from speech-to-text + final RxString visionResponse = + "".obs; // Store the image response that will received by gemini model + final RxString _currentChatBoxID = + "".obs; // to store current chat box where user send prompt + final RxString filePath = + "".obs; // file path of selected pdf file + final RxBool _speechEnabled = + false.obs; // Flag to track if speech recognition is enabled + final RxBool speechListen = + false.obs; // Flag to indicate if app is actively listening to user speech + final RxBool isTextPrompt = + false.obs; // Flag to indicate if a text response is received + final RxBool isLoading = + false.obs; // Flag to show loading state when waiting for API response + final RxBool isImagePrompt = + false.obs; // Flag to determine if text-to-speech should stop + final RxBool shouldTextAnimate = + false.obs; // Flag to indicate the response text should re animate or not + final RxBool isStopped = + true.obs; // Flag to determine if text-to-speech should stop + final RxBool isNewPrompt = + true.obs; // Flag to determine whether current prompt is new or old chat prompt + final RxList messages = + [].obs; // List to hold conversation messages + final RxList totalChatBoxes = + [].obs; // List of all the previous chat/prompt box/folder + final RxList imagesFileList = + [].obs; // List of selected image files + + // Non-observable State + final PageController _pageController = + PageController(); // PageView controller + final List _messageQueue = + []; // Queue for storing messages to be spoken by text-to-speech + String _chatBoxTitle = ""; // to store the title of new prompt chat box + + HomeController({ + required ChatData chatData, + required GenerateContentUseCase generateContent, + }) : _generateContent = generateContent, + _chatData = chatData; + + // Current page index getter + String get currentChatBoxID => _currentChatBoxID.value; + PageController get pageController => _pageController; + int get currentAllChatBoxes => _chatData.getAllChatBoxes().length; + + // Update current page index + void currentIndexValue(int value) { + currentIndex.value = value; + } + + // set current chat box id + void setCurrentChatId({required String newChatId}) { + _currentChatBoxID.value = newChatId; + } + + // set current chat box title + void changeChatBoxTitle({required String newChatTitle, String? chatId}) { + _chatBoxTitle = newChatTitle; + if (chatId != null) { + final chat = _chatData.chatBox.get(chatId); + chat!.title = _chatBoxTitle; + chat.save().then( + (value) => {totalChatBoxes.value = _chatData.getAllChatBoxes()}); + } + } + + void initializeTotalChatBoxes({required bool firstTime}) { + totalChatBoxes.value = _chatData.getAllChatBoxes(); + if (!firstTime && totalChatBoxes.isNotEmpty) { + totalChatBoxes.removeLast(); + } + initialAllChatBoxes.value = totalChatBoxes.length; + } + + Future deleteChatBox({required String chatId}) async { + bool b = await _chatData.deleteChatBox(chatId: chatId); + if (b) { + initializeTotalChatBoxes(firstTime: false); + } else { + AlertMessages.showSnackBar( + "Hive Error: something went wrong to deleting this ChatBox"); + } + } + + @override + void onInit() { + super.onInit(); + initialize(); // Initialize greeting and speech services + initializeTotalChatBoxes(firstTime: true); + } + + @override + void onClose() { + stopTTs(); // Stops any active text-to-speech + stopListening(); // Stops any active speech-to-text + } + + // Initializes greeting message, speech recognition, and TTS settings + Future initialize() async { + await speechInitialize(); + await _flutterTts.setLanguage("en-US"); // Sets TTS language + await _flutterTts.setSpeechRate(0.5); // Sets TTS speaking speed + await _flutterTts.setVolume(1.0); // Sets TTS volume + await _flutterTts.setPitch(1.0); // Sets TTS pitch + _flutterTts.setCompletionHandler( + _onSpeakCompleted); // Sets a handler for TTS completion + } + + Future askPermission() async { + // Asks for microphone permission and opens settings if denied + var requestStatus = await Permission.microphone.request(); + if (requestStatus.isDenied || requestStatus.isPermanentlyDenied) { + await openAppSettings(); + } else if (requestStatus.isGranted) { + speechInitialize(); // Initializes speech recognition if permission is granted + } + } + + // Callback to handle changes in the speech recognition status + void _onSpeechStatus(String status) async { + if (status == "notListening") { + speechListen.value = false; // Updates flag when user stops speaking + await Future.delayed(const Duration( + seconds: + 2)); // here delay is used to store the speech words, if not it will miss the last word of your prompt + _sendRequest(_userVoiceMsg.value); // Process the captured input + stopListening(); // Stops speech recognition + } + } + + // Called when TTS completes a message, then checks for more messages in queue + void _onSpeakCompleted() { + if (!isStopped.value) { + _speakNextMessage(); // Speak the next message if not stopped + } + } + + // Initializes the message queue for speaking and starts TTS + void playTTs() async { + for (var message in messages) { + if (message.imagePath == null && message.filePath == null) _messageQueue.add(message.text); + } + isStopped.value = false; + _flutterTts.setCompletionHandler(_onSpeakCompleted); + await _speakNextMessage(); // Begins speaking messages in the queue + } + + // Resets conversation messages and stops both TTS and speech recognition + void resetAll() { + messages.value = []; + _messageQueue.clear(); + initializeTotalChatBoxes(firstTime: true); + isImagePrompt.value = false; + isNewPrompt.value = true; + checkAlreadyCreated(); + stopTTs(); + stopListening(); + } + + // Initializes speech recognition and sets error handlers + Future speechInitialize() async { + _speechEnabled.value = await speech.initialize( + onStatus: (status) => + _onSpeechStatus(status), // Sets status change handler + onError: (error) => AlertMessages.showSnackBar( + error.errorMsg) // Shows error on initialization failure + ); + if (!_speechEnabled.value) { + AlertMessages.audioBottomSheet( + "Speech recognition is not available on this device."); + } + } + + // Speaks the next message in the queue if available + Future _speakNextMessage() async { + if (_messageQueue.isNotEmpty && !isStopped.value) { + await _flutterTts + .speak(_messageQueue.removeAt(0)); // Speaks the next message in queue + } else { + isStopped.value = true; // Sets stopped flag when queue is empty + } + } + + // Adds a message to the queue and starts TTS + Future speakTTs(String botResponse) async { + isStopped.value = false; + _messageQueue.add(botResponse); + await _speakNextMessage(); + } + + // Adds a message to the queue and starts TTS + Future stopTTs() async { + isStopped.value = true; + await _flutterTts.stop(); + } + + // Each time to start a speech recognition session + Future startListening() async { + speechListen.value = true; + await speech.listen( + onResult: (result) { + _userVoiceMsg.value = + result.recognizedWords; // Captures user's speech as text + }, + listenOptions: stt.SpeechListenOptions( + partialResults: true, + listenMode: stt.ListenMode + .dictation, // Use dictation mode for continuous listening + cancelOnError: true), + pauseFor: const Duration(seconds: 2), + ); + } + + /// Manually stop the active speech recognition session Note that there are also timeouts that each platform enforces + /// and the SpeechToText plugin supports setting timeouts on the listen method. + Future stopListening() async { + _userVoiceMsg.value = ""; + speechListen.value = false; + await speech.stop(); + } + + // Sends user input to appropriate API and speaks the response if needed + Future _sendRequest(String input) async { + try { + isTextPrompt.value = true; + if (input.isNotEmpty) { + messages.add(HiveChatBoxMessages( + text: input, + isUser: true, + imagePath: null, + )); + isLoading.value = true; + + final response = await _service.isArtPromptAPI(input); + if (response == "YES") { + await callImagineAPI( + input); // Calls Imagine API if input asks for an image + } else { + await sendPrompt( + input); // Calls Gemini API if text response is expected + } + } else { + // Adds a default prompt message when no input is provided + isTextPrompt.value = true; + messages.add(HiveChatBoxMessages( + text: + "Please provide me with some context or a question so I can assist you.", + isUser: true)); + _messageQueue.add( + "Please provide me with some context or a question so I can assist you."); + isStopped.value = false; + await _speakNextMessage(); + shouldTextAnimate.value = true; + messages.add(HiveChatBoxMessages( + text: "For example: Give me some Interview Tips.", isUser: false)); + _messageQueue.add("For example: Give me some Interview Tips."); + isStopped.value = false; + await _speakNextMessage(); + } + } on AppException catch (e) { + isLoading.value = false; + messages.add(HiveChatBoxMessages(text: "Failed", isUser: false)); + AlertMessages.showSnackBar(e.message.toString()); + } catch (e) { + isLoading.value = false; + messages.add(HiveChatBoxMessages(text: "Failed", isUser: false)); + AlertMessages.showSnackBar(e.toString()); + } + } + + Future sendPrompt(String prompt) async { + isImagePrompt.value = false; + + if (imagesFileList.isNotEmpty) { + messages.add(HiveChatBoxMessages( + text: prompt, + isUser: true, + imagePath: (imagesFileList.isNotEmpty) + ? List.from(imagesFileList) + : null, + filePath: null)); + isLoading.value = true; + } + + try { + final Stream response = + await _generateContent.execute( + prompt: prompt, + history: await getHistoryMessages(), + isTextOnly: imagesFileList.isEmpty, + images: imagesFileList.map((image) => XFile(image)).toList()); + response.listen((event) { + visionResponse.value += event.text.toString(); + }, onDone: () async { + isLoading.value = false; // Ends loading state + if (visionResponse.value.isNotEmpty) { + messages.add( + HiveChatBoxMessages(text: visionResponse.value, isUser: false)); + speakTTs(visionResponse.value); + visionResponse.value = ""; + } + if (isNewPrompt.value && + messages.length == 2 && + _chatBoxTitle.isEmpty) { + setChatBoxTitle(); + } + saveMessagesInDB(); + // save message to hive db + }).onError((error, stackTrace) { + isLoading.value = false; // Ends loading state + AlertMessages.showSnackBar(error.toString()); + messages.add(HiveChatBoxMessages(text: error.toString(), isUser: false)); + speakTTs(error.toString()); + }); + } catch (e) { + isLoading.value = false; + AlertMessages.showSnackBar(e.toString()); + messages.add(HiveChatBoxMessages(text: "Failed", isUser: false)); + } finally { + imagesFileList.clear(); + } + } + + // Calls the Imagine API to fetch an image based on input text + Future callImagineAPI(String input) async { + try { + final data = await _service.imagineAPI(input); + isLoading.value = false; + messages.add(HiveChatBoxMessages( + text: "Here, is a comprehensive desire image output of your prompt.", isUser: false, imagePath: [data], filePath: null)); + speakTTs("Here, is a comprehensive desire image output of your prompt."); + if (isNewPrompt.value && messages.length == 2 && _chatBoxTitle.isEmpty) { + setChatBoxTitle(); + } + saveMessagesInDB(); + } on AppException catch (e) { + // Adds an error message if the call fails + isLoading.value = false; + messages.add(HiveChatBoxMessages(text: "Failed", isUser: false)); + AlertMessages.showSnackBar(e.message.toString()); + } catch (e) { + // Adds an error message if the call fails + isLoading.value = false; + messages.add(HiveChatBoxMessages(text: "Failed", isUser: false)); + AlertMessages.showSnackBar(e.toString()); + } + } + + Future?> getHistoryMessages() async { + List result = []; + try { + final history = _chatData.getChatHistory(currentChatBoxID); + if (history != null) { + Content content; + for (var data in history) { + content = (data.isUser && data.imagePath == null) + ? Content("user", [TextPart(data.text)]) + : (data.isUser && data.imagePath != null) + ? Content("user", [ + ...await getDataPartList(data.imagePath! + .map((value) => XFile(value)) + .toList()), + TextPart(data.text) + ]) + : (!data.isUser && data.imagePath == null) + ? Content("model", [TextPart(data.text)]) + : Content( + "model", + await getDataPartList(data.imagePath! + .map((value) => XFile(value)) + .toList())); + result.add(content); + } + return result; + } else { + return null; + } + } catch (e) { + AlertMessages.showSnackBar(e.toString()); + return null; + } + } + + void saveMessagesInDB() { + shouldTextAnimate.value = true; + Future.delayed(const Duration(seconds: 3), + () => _chatData.saveMessage(currentChatBoxID, _chatBoxTitle, messages)); + } + + void checkAlreadyCreated() { + if (initialAllChatBoxes.value < currentAllChatBoxes) { + setCurrentChatId(newChatId: _chatData.getLastChatBox().id); + changeChatBoxTitle(newChatTitle: _chatData.getLastChatBox().title); + final history = _chatData.getChatHistory(currentChatBoxID); + if (history != null) { + messages.value = history.map((e) => e).toList(); + } + isTextPrompt.value = true; + } else { + setCurrentChatId(newChatId: DateTime.now().toIso8601String()); + _chatBoxTitle = ""; + setGreeting(); + isTextPrompt.value = false; + messages.value = []; + } + } + + Future pickImage() async { + final pickedImages = await ImagePicker() + .pickMultiImage(maxHeight: 800, maxWidth: 800, imageQuality: 100); + if (pickedImages.isNotEmpty) { + isImagePrompt.value = true; + isTextPrompt.value = true; + imagesFileList.value = pickedImages.map((image) => image.path).toList(); + if (filePath.value.isNotEmpty) { + filePath.value = ""; + } + } + } + + Future pickFile() async { + FilePickerResult? result = await FilePicker.platform.pickFiles( + allowMultiple: false, + type: FileType.custom, + allowedExtensions: ['pdf']); + + if (result != null) { + isImagePrompt.value = true; + isTextPrompt.value = true; + filePath.value = result.files.single.path!; + if (imagesFileList.isNotEmpty) { + imagesFileList.value = []; + } + } + } + + Future> getDataPartList(List images) async { + final imageFutures = images + .map((imageFile) => imageFile.readAsBytes()) + .toList(growable: false); + + final imageBytes = await Future.wait(imageFutures); + final imageParts = imageBytes + .map((bytes) => DataPart('image/jpeg', Uint8List.fromList(bytes))) + .toList(); + return imageParts; + } + + Future setChatBoxTitle() async { + String prompt = messages[0].text; + String response = messages[1].text; + _chatBoxTitle = ChatTitleGenerator.generateTitle(prompt, response); + } + + void setGreeting() { + // Sets initial greeting based on time of day + final hour = DateTime.now().hour; + if (hour < 12) { + greetingMessage.value = "Good Morning"; + } else if (hour < 18) { + greetingMessage.value = "Good Afternoon"; + } else { + greetingMessage.value = "Good Evening"; + } + } + + Future uploadPdf(String text) async { + isImagePrompt.value = false; + final fileName = filePath.value.split('/').last.split('-').last; + messages.add(HiveChatBoxMessages( + text: fileName, + isUser: true, + imagePath: null, + filePath: (filePath.isNotEmpty) ? filePath.value : null)); + isLoading.value = true; + + try { + final response = await _generateContent.sendPromptFile( + prompt: text, file: File(filePath.value), fileName: fileName); + if (response.isNotEmpty) { + isLoading.value = false; + messages.add(HiveChatBoxMessages(text: response, isUser: false)); + speakTTs(response); + if (isNewPrompt.value && + messages.length == 2 && + _chatBoxTitle.isEmpty) { + setChatBoxTitle(); + } + saveMessagesInDB(); + } + } catch (e) { + isLoading.value = false; + AlertMessages.showSnackBar(e.toString()); + messages.add(HiveChatBoxMessages(text: "Failed", isUser: false)); + } finally { + filePath.value = ""; + } + } +} diff --git a/lib/presentation/views/home_screen.dart b/lib/presentation/views/home_screen.dart new file mode 100644 index 0000000..2717f55 --- /dev/null +++ b/lib/presentation/views/home_screen.dart @@ -0,0 +1,114 @@ +import 'package:animate_do/animate_do.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:hive/hive.dart'; +import 'package:voice_assistant/domain/usecases/generate_content.dart'; +import 'package:voice_assistant/presentation/views/prompt_screen.dart'; +import 'package:voice_assistant/presentation/views/previous_prompts.dart'; +import 'package:voice_assistant/utils/alert_messages.dart'; + +import '../../data/hivedata/chat_data.dart'; +import '../controllers/home_controller.dart'; + +class HomeScreen extends StatelessWidget { + HomeScreen({super.key}); + + final controller = Get.put( + HomeController( + chatData: ChatData(chatBox: Hive.box('ChatBoxesHistories')), + generateContent: GenerateContentUseCase()), + permanent: true); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: BounceInDown( + child: const Text( + "Voice Genie", + style: TextStyle(fontWeight: FontWeight.w400, fontFamily: "Cera"), + )), + actions: [ + Obx(() => (controller.currentIndex.value == 0) + ? IconButton( + icon: const Icon( + Icons.attach_file, + color: Colors.white, + ), + onPressed: () async { + bool b = await AlertMessages.getStoragePermission(); + if (b) { + controller.pickFile(); + } else { + AlertMessages.alertPermission(context); + } + }) + : const SizedBox.shrink()), + Obx(() => (controller.currentIndex.value == 0) + ? IconButton( + icon: const Icon( + Icons.photo_library, + color: Colors.white, + ), + onPressed: () async { + bool b = await AlertMessages.getStoragePermission(); + if (b) { + controller.pickImage(); + } else { + AlertMessages.alertPermission(context); + } + }) + : const SizedBox.shrink()) + ], + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.blue.shade300, Colors.lightGreenAccent.shade100], + ), + ), + ), + ), + body: PageView( + controller: controller.pageController, + onPageChanged: (index) { + controller.currentIndexValue(index); + }, + children: const [ + PromptScreen(), + PreviousPromptsScreen(), + ], + ), + bottomNavigationBar: Obx( + () => BottomNavigationBar( + elevation: 12.0, + unselectedItemColor: Colors.grey, + selectedItemColor: Colors.tealAccent.shade400, + currentIndex: controller.currentIndex.value, + onTap: (index) { + controller.pageController.animateToPage( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + controller.currentIndexValue(index); + }, + items: const [ + BottomNavigationBarItem( + icon: Icon( + Icons.chat, + ), + label: 'Prompt', + ), + BottomNavigationBarItem( + icon: Icon( + Icons.history, + ), + label: 'History'), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/views/previous_prompts.dart b/lib/presentation/views/previous_prompts.dart new file mode 100644 index 0000000..35ebf46 --- /dev/null +++ b/lib/presentation/views/previous_prompts.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:voice_assistant/presentation/views/prompt_history.dart'; +import 'package:voice_assistant/utils/alert_messages.dart'; + +import '../controllers/home_controller.dart'; + +class PreviousPromptsScreen extends StatefulWidget { + const PreviousPromptsScreen({ + super.key, + }); + + @override + State createState() => _PreviousPromptsScreenState(); +} + +class _PreviousPromptsScreenState extends State { + final controller = Get.find(); + final textFocus = FocusScopeNode(); + final textController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Obx( + () => controller.totalChatBoxes.isEmpty + ? const Center( + child: Text("No Chat/Prompt Box Created Yet!"), + ) + : ListView.builder( + padding: const EdgeInsets.symmetric(vertical: 4.0), + itemCount: controller.totalChatBoxes.length, + itemBuilder: (context, index) { + final chat = controller.totalChatBoxes[index]; + return InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PromptHistoryScreen( + promptId: chat.id, + promptTitle: chat.title, + promptMessages: chat.messages, + ctrl: controller))); + }, + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 10.0, vertical: 6.0), + padding: const EdgeInsets.symmetric( + horizontal: 8.0, vertical: 4.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.0), + border: Border.all( + color: Colors.lightBlue, + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.shade200, + blurRadius: 2.0, + spreadRadius: 1.0), + const BoxShadow( + color: Colors.black38, + blurRadius: 4.0, + spreadRadius: 1.0) + ], + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade300, + Colors.lightGreenAccent.shade100 + ], + ), + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + const CircleAvatar( + backgroundImage: + AssetImage("assets/images/botImage.jpg")), + Flexible( + child: Center( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + chat.title, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontFamily: 'Cera', + fontSize: 18, + color: Colors.white, + fontWeight: FontWeight.w500), + ), + ), + ), + ), + InkWell( + highlightColor: Colors.redAccent, + onTap: () { + AlertMessages.titleDialog( + textController, context, textFocus, chat.id); + textFocus.requestFocus(); + }, + child: const Icon( + Icons.edit_rounded, + color: Colors.lightBlue, + )), + IconButton( + color: Colors.lightBlue, + highlightColor: Colors.redAccent, + padding: const EdgeInsets.all(0), + onPressed: () => + AlertMessages.deleteDialog(context, chat.id), + icon: const Icon( + Icons.delete_forever_rounded, + )), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/presentation/views/prompt_history.dart b/lib/presentation/views/prompt_history.dart new file mode 100644 index 0000000..15a6dab --- /dev/null +++ b/lib/presentation/views/prompt_history.dart @@ -0,0 +1,178 @@ +import 'package:animate_do/animate_do.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; +import 'package:get/get.dart'; +import 'package:loading_animation_widget/loading_animation_widget.dart'; +import 'package:voice_assistant/data/adapters/models_adapter.dart'; +import 'package:voice_assistant/presentation/controllers/home_controller.dart'; +import 'package:voice_assistant/widgets/prompt_container.dart'; +import 'package:voice_assistant/widgets/prompt_messages.dart'; +import 'package:voice_assistant/widgets/virtual_assistant_image.dart'; + +import '../../widgets/image_prompt.dart'; +import '../../widgets/multiple_floating.dart'; + +class PromptHistoryScreen extends StatefulWidget { + const PromptHistoryScreen( + {super.key, + required this.promptId, + required this.promptTitle, + required this.promptMessages, + required this.ctrl}); + + final List promptMessages; + final String promptId; + final String promptTitle; + final HomeController ctrl; + + @override + State createState() => _PromptHistoryScreenState(); +} + +class _PromptHistoryScreenState extends State { + final GlobalKey fabKey2 = GlobalKey(); + final _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + widget.ctrl.isNewPrompt.value = false; + widget.ctrl.setCurrentChatId(newChatId: widget.promptId); + widget.ctrl.changeChatBoxTitle(newChatTitle: widget.promptTitle); + widget.ctrl.messages.value = widget.promptMessages.map((e) => e).toList(); + } + + @override + void dispose() { + super.dispose(); + widget.ctrl.setCurrentChatId(newChatId: ""); + widget.ctrl.changeChatBoxTitle(newChatTitle: ""); + widget.ctrl.isNewPrompt.value = true; + _scrollController.dispose(); + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients && + _scrollController.position.maxScrollExtent > 0.0) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(seconds: 1), + curve: Curves.easeOut, + ); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + centerTitle: true, + leading: IconButton( + onPressed: Navigator.of(context).pop, + icon: const Icon( + Icons.arrow_back, + color: Colors.white, + )), + title: BounceInDown( + child: Text( + widget.promptTitle, + style: + const TextStyle(fontWeight: FontWeight.w400, fontFamily: "Cera"), + )), + actions: [ + IconButton( + icon: const Icon( + Icons.attach_file, + color: Colors.white, + ), + onPressed: () { + widget.ctrl.pickFile(); + }, + ), + IconButton( + icon: const Icon( + Icons.photo_library, + color: Colors.white, + ), + onPressed: () { + widget.ctrl.pickImage(); + }, + ) + ], + flexibleSpace: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.blue.shade300, Colors.lightGreenAccent.shade100], + ), + ), + ), + ), + floatingActionButton: Obx( + () => (widget.ctrl.isImagePrompt.value) + ? navigatingFloating() + : MultipleFloating(fabKey: fabKey2), + ), + body: SingleChildScrollView( + controller: _scrollController, + child: Column(children: [ + SizedBox( + height: Get.height * 0.02, + ), + const VirtualAssistantImage(), + Obx(() { + if (widget.ctrl.isImagePrompt.value) { + return const ImagePrompt(); + } else { + Future.microtask(_scrollToBottom); + return PromptContainer( + child: (widget.ctrl.messages.isEmpty) + ? Align( + alignment: Alignment.centerRight, + child: LoadingAnimationWidget.progressiveDots( + color: Colors.grey.shade400, size: 40)) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PromptMessagesWidget( + ctrl: widget.ctrl, + message: widget.ctrl.messages), + (widget.ctrl.isLoading.value) + ? Align( + alignment: Alignment.centerLeft, + child: + LoadingAnimationWidget.progressiveDots( + color: Colors.grey.shade400, + size: 40), + ) + : const SizedBox.shrink() + ], + )); + } + }), + ]), + ), + ); + } + + Widget navigatingFloating() { + return FloatingActionButton( + onPressed: () async { + widget.ctrl.isImagePrompt.value = false; + widget.ctrl.isTextPrompt.value = false; + widget.ctrl.imagesFileList.clear(); + }, + shape: + const CircleBorder(side: BorderSide(color: Colors.white, width: 2)), + backgroundColor: Colors.tealAccent.shade400, + child: const Icon( + Icons.arrow_back, + color: Colors.white, + ), + ); + } +} diff --git a/lib/presentation/views/prompt_screen.dart b/lib/presentation/views/prompt_screen.dart new file mode 100644 index 0000000..1127c69 --- /dev/null +++ b/lib/presentation/views/prompt_screen.dart @@ -0,0 +1,199 @@ +import 'package:animate_do/animate_do.dart'; +import 'package:flutter/material.dart'; +import 'package:animated_text_kit/animated_text_kit.dart'; +import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; +import 'package:loading_animation_widget/loading_animation_widget.dart'; +import 'package:get/get.dart'; +import 'package:voice_assistant/widgets/prompt_messages.dart'; + +import '../../widgets/feature_box.dart'; +import '../../widgets/image_prompt.dart'; +import '../../widgets/multiple_floating.dart'; +import '../../widgets/prompt_container.dart'; +import '../../widgets/virtual_assistant_image.dart'; +import '../controllers/home_controller.dart'; + +class PromptScreen extends StatefulWidget { + const PromptScreen({super.key}); + + @override + State createState() => _PromptScreenState(); +} + +class _PromptScreenState extends State { + final ctrl = Get.find(); + final _scrollController = ScrollController(); + final fabKey1 = GlobalKey(); + + @override + void initState() { + super.initState(); + ctrl.checkAlreadyCreated(); + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients && + _scrollController.position.maxScrollExtent > 0.0) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(seconds: 1), + curve: Curves.easeOut, + ); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + floatingActionButtonLocation: ExpandableFab.location, + floatingActionButton: Align( + alignment: Alignment.bottomRight, + child: ZoomIn( + delay: const Duration(milliseconds: 800), + duration: const Duration(milliseconds: 600), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Obx( + () => (ctrl.isImagePrompt.value) + ? navigatingFloating() + : MultipleFloating(fabKey: fabKey1), + ), + ), + ), + ), + body: SingleChildScrollView( + controller: _scrollController, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + SizedBox( + height: Get.height * 0.02, + ), + const VirtualAssistantImage(), + FadeIn( + delay: const Duration(milliseconds: 200), + duration: const Duration(milliseconds: 600), + child: Obx(() => (ctrl.isImagePrompt.value) + ? const ImagePrompt() + : (!ctrl.isTextPrompt.value && ctrl.messages.isEmpty) + ? initialPrompt() + : promptSpace())), + Obx( + () => Visibility( + visible: !ctrl.isTextPrompt.value, + child: SlideInLeft( + delay: const Duration(milliseconds: 200), + duration: const Duration(milliseconds: 600), + child: Container( + padding: const EdgeInsets.all(10), + alignment: Alignment.centerLeft, + margin: const EdgeInsets.symmetric(horizontal: 24.0), + child: Text( + 'Here are a few features', + style: TextStyle( + fontFamily: 'Cera', + color: Colors.indigo.shade800, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ), + Obx( + () => Visibility( + visible: !ctrl.isTextPrompt.value, + child: Column( + children: [ + SlideInRight( + delay: const Duration(milliseconds: 400), + duration: const Duration(milliseconds: 600), + child: const FeatureBox( + headerText: 'Gemini', + descriptionText: + 'A smarter way to stay organized and informed with Gemini AI', + ), + ), + SlideInLeft( + delay: const Duration(milliseconds: 600), + duration: const Duration(milliseconds: 600), + child: const FeatureBox( + headerText: 'Imagine AI', + descriptionText: + 'Get inspired and stay creative with your personal assistant powered by Imagine, Your commercial Generative AI solution', + ), + ), + SlideInRight( + delay: const Duration(milliseconds: 800), + duration: const Duration(milliseconds: 600), + child: const FeatureBox( + headerText: 'Smart Voice Assistant', + descriptionText: + 'Get the best of both worlds with a voice assistant powered by Imagine and Gemini', + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget initialPrompt() { + return PromptContainer( + child: AnimatedTextKit( + key: UniqueKey(), + animatedTexts: [ + TyperAnimatedText("${ctrl.greetingMessage}, what can I do for you?", + textStyle: const TextStyle( + fontFamily: 'Cera', color: Colors.black54, fontSize: 16), + textAlign: TextAlign.start), + ], + isRepeatingAnimation: false, + )); + } + + Widget promptSpace() { + Future.microtask(_scrollToBottom); + return PromptContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PromptMessagesWidget(message: ctrl.messages, ctrl: ctrl), + (ctrl.isLoading.value) + ? Align( + alignment: Alignment.centerLeft, + child: LoadingAnimationWidget.progressiveDots( + color: Colors.grey.shade400, size: 40), + ) + : const SizedBox.shrink() + ], + ), + ); + } + + Widget navigatingFloating() { + return FloatingActionButton( + onPressed: () async { + if (ctrl.messages.isEmpty) { + ctrl.isTextPrompt.value = false; + } + ctrl.isImagePrompt.value = false; + ctrl.imagesFileList.clear(); + }, + shape: + const CircleBorder(side: BorderSide(color: Colors.white, width: 2)), + backgroundColor: Colors.tealAccent.shade400, + child: const Icon( + Icons.arrow_back, + color: Colors.white, + ), + ); + } +} diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart deleted file mode 100644 index e64877f..0000000 --- a/lib/screens/home_screen.dart +++ /dev/null @@ -1,350 +0,0 @@ -import 'dart:io'; - -import 'package:animate_do/animate_do.dart'; -import 'package:animated_text_kit/animated_text_kit.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; -import 'package:get/get.dart'; -import 'package:loading_animation_widget/loading_animation_widget.dart'; - -import '../controllers/home_controller.dart'; -import '../widgets/feature_box.dart'; - -class HomeScreen extends StatelessWidget { - HomeScreen({super.key}); - - final controller = Get.put(HomeController(), permanent: true); - final _key = GlobalKey(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: BounceInDown( - child: const Text( - "Voice Genie", - style: TextStyle(fontWeight: FontWeight.w400, fontFamily: "Cera"), - )), - flexibleSpace: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Colors.blue.shade300, Colors.lightGreenAccent.shade100], - ), - ), - ), - ), - floatingActionButtonLocation: ExpandableFab.location, - floatingActionButton: Align( - alignment: Alignment.bottomRight, - child: ZoomIn( - delay: const Duration(milliseconds: 600), - duration: const Duration(milliseconds: 600), - child: Obx(() => (controller.textResponse.value == false) - ? Padding( - padding: const EdgeInsets.only(bottom: 8.0, right: 16.0), - child: FloatingActionButton( - onPressed: () async { - if (await controller.speech.hasPermission && - controller.speech.isNotListening) { - await controller.startListening(); - } else if (controller.speech.isListening) { - await controller.stopListening(); - } else { - controller.initialize(); - } - }, - shape: const CircleBorder( - side: BorderSide(color: Colors.white, width: 2)), - backgroundColor: Theme.of(context).primaryColor, - child: Obx( - () => Icon( - (controller.speechListen.value) - ? Icons.stop - : Icons.mic, - color: Colors.white, - ), - ), - ), - ) - : ExpandableFab( - key: _key, - fanAngle: 90, - distance: 80, - openButtonBuilder: RotateFloatingActionButtonBuilder( - child: const Icon(Icons.menu_open_rounded), - fabSize: ExpandableFabSize.regular, - foregroundColor: Colors.white, - backgroundColor: Theme.of(context).primaryColor, - shape: const CircleBorder(), - ), - closeButtonBuilder: RotateFloatingActionButtonBuilder( - child: const Icon(Icons.close), - fabSize: ExpandableFabSize.small, - foregroundColor: Colors.white, - backgroundColor: Theme.of(context).primaryColor, - shape: const CircleBorder(), - ), - children: [ - FloatingActionButton( - onPressed: () async { - if (await controller.speech.hasPermission && - controller.speech.isNotListening) { - await controller.startListening(); - } else if (controller.speech.isListening) { - await controller.stopListening(); - } else { - controller.initialize(); - } - }, - shape: const CircleBorder( - side: BorderSide(color: Colors.white, width: 2)), - backgroundColor: Theme.of(context).primaryColor, - child: Obx( - () => Icon( - (controller.speechListen.value) - ? Icons.stop - : Icons.mic, - color: Colors.white, - ), - ), - ), - FloatingActionButton( - onPressed: () { - (controller.isStopped.value) - ? controller.playTTs() - : controller.stopTTs(); - }, - shape: const CircleBorder( - side: BorderSide(color: Colors.white, width: 2)), - backgroundColor: Theme.of(context).primaryColor, - child: Icon( - (controller.isStopped.value) - ? Icons.play_arrow - : Icons.stop, - color: Colors.white, - ), - ), - FloatingActionButton( - onPressed: () { - final state = _key.currentState; - if (state != null) { - state.toggle(); - controller.resetAll(); - } - }, - shape: const CircleBorder( - side: BorderSide(color: Colors.white, width: 2)), - backgroundColor: Theme.of(context).primaryColor, - child: const Icon( - Icons.restart_alt_rounded, - color: Colors.white, - ), - ) - ], - )), - ), - ), - body: SingleChildScrollView( - child: Column( - children: [ - SizedBox( - height: Get.height * 0.02, - ), - ZoomIn( - duration: const Duration(milliseconds: 600), - child: Stack( - children: [ - Center( - child: Container( - height: 125, - width: 125, - margin: const EdgeInsets.only(top: 4), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 5), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Colors.blue.shade300, - Colors.lightGreenAccent.shade100 - ], - ), - ), - ), - ), - Container( - height: 125, - decoration: const BoxDecoration( - shape: BoxShape.circle, - image: DecorationImage( - image: AssetImage( - 'assets/images/virtualAssistant.png', - ), - ), - ), - ), - ], - ), - ), - FadeIn( - duration: const Duration(milliseconds: 600), - child: Obx( - () => Container( - width: Get.width, - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 10), - margin: const EdgeInsets.symmetric( - horizontal: 30, vertical: 20), - decoration: BoxDecoration( - border: Border.all( - color: Colors.blueGrey.shade600, width: 1.25), - borderRadius: BorderRadius.circular(24).copyWith( - topLeft: Radius.zero, bottomRight: Radius.zero), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - (controller.messages.isEmpty) - ? AnimatedTextKit( - key: ValueKey(controller.textResponse.value), - animatedTexts: [ - TyperAnimatedText( - "${controller.greetingMessage}, what can I do for you?", - textStyle: const TextStyle( - fontFamily: 'Cera', - color: Colors.black54, - fontSize: 16), - textAlign: TextAlign.start), - ], - isRepeatingAnimation: false, - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: controller.messages.map((msg) { - if (msg.isImage) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: ClipRRect( - borderRadius: - BorderRadius.circular(20), - child: Image.file( - File(msg.parts.first.text)), - ), - ); - } else { - return Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - AnimatedTextKit( - key: ValueKey( - controller.textResponse.value), - displayFullTextOnTap: true, - animatedTexts: [ - TyperAnimatedText( - (msg.role == "user") - ? "Q. ${msg.parts.first.text}" - : msg.parts.first.text, - textStyle: TextStyle( - fontFamily: 'Cera', - color: (msg.role == - "user") - ? Colors.black87 - : (msg.parts.first - .text == - "Failed") - ? Colors.red - : Colors.grey, - fontSize: 16), - textAlign: - (msg.role == "model" && - msg.parts.first - .text == - "Failed") - ? TextAlign.end - : TextAlign.start), - ], - isRepeatingAnimation: false, - ), - ], - ); - } - }).toList()), - (controller.isLoading.value) - ? Align( - alignment: Alignment.centerLeft, - child: LoadingAnimationWidget.progressiveDots( - color: Colors.grey.shade400, size: 40), - ) - : const SizedBox.shrink() - ])), - ), - ), - Obx( - () => Visibility( - visible: !controller.textResponse.value, - child: SlideInLeft( - delay: const Duration(milliseconds: 100), - duration: const Duration(milliseconds: 600), - child: Container( - padding: const EdgeInsets.all(10), - alignment: Alignment.centerLeft, - margin: const EdgeInsets.symmetric(horizontal: 24.0), - child: Text( - 'Here are a few features', - style: TextStyle( - fontFamily: 'Cera', - color: Colors.indigo.shade800, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ), - ), - Obx( - () => Visibility( - visible: !controller.textResponse.value, - child: Column( - children: [ - SlideInRight( - delay: const Duration(milliseconds: 200), - duration: const Duration(milliseconds: 600), - child: const FeatureBox( - headerText: 'Gemini', - descriptionText: - 'A smarter way to stay organized and informed with Gemini AI', - ), - ), - SlideInLeft( - delay: const Duration(milliseconds: 400), - duration: const Duration(milliseconds: 600), - child: const FeatureBox( - headerText: 'Imagine AI', - descriptionText: - 'Get inspired and stay creative with your personal assistant powered by Imagine, Your commercial Generative AI solution', - ), - ), - SlideInRight( - delay: const Duration(milliseconds: 600), - duration: const Duration(milliseconds: 600), - child: const FeatureBox( - headerText: 'Smart Voice Assistant', - descriptionText: - 'Get the best of both worlds with a voice assistant powered by Imagine and Gemini', - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/utils/alert_messages.dart b/lib/utils/alert_messages.dart index fbc9125..230cf70 100644 --- a/lib/utils/alert_messages.dart +++ b/lib/utils/alert_messages.dart @@ -1,9 +1,53 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:voice_assistant/controllers/home_controller.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import '../presentation/controllers/home_controller.dart'; class AlertMessages { - static Widget bottomSheet({required String msg}) { + static void showSnackBar(String message, {int? duration}) { + Get.showSnackbar(GetSnackBar( + message: message, + duration: Duration( + seconds: duration ?? 5, + ), + backgroundGradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.blue.shade300, Colors.lightGreenAccent.shade100], + ), + boxShadows: [ + BoxShadow( + color: Colors.black.withOpacity(0.4), + blurRadius: 10, + offset: const Offset(0, 6), + ), + ], + snackPosition: SnackPosition.BOTTOM, + borderRadius: 20, + margin: const EdgeInsets.all(12), + isDismissible: true, + dismissDirection: DismissDirection.horizontal, + forwardAnimationCurve: Curves.easeOutBack, + icon: const Icon( + Icons.error, + color: Colors.white, + ), + borderColor: Colors.white, + borderWidth: 2)); + } + + static Future audioBottomSheet(String error) { + return Get.bottomSheet( + elevation: 8.0, + ignoreSafeArea: true, + persistent: true, + isDismissible: false, + enableDrag: false, + permissionCard(msg: "Error: $error")); + } + + static Widget permissionCard({required String msg}) { return Card( elevation: 8, shadowColor: Colors.grey[400], @@ -77,35 +121,175 @@ class AlertMessages { ); } - static void showSnackBar(String message, {int? duration}) { - Get.showSnackbar(GetSnackBar( - message: message, - duration: Duration( - seconds: duration ?? 5, - ), - backgroundGradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Colors.blue.shade300, Colors.lightGreenAccent.shade100], - ), - boxShadows: [ - BoxShadow( - color: Colors.black.withOpacity(0.4), - blurRadius: 10, - offset: const Offset(0, 6), + static Future getStoragePermission() async { + var status = await Permission.storage.status; + if (status.isGranted) { + return true; + } else if (status.isDenied) { + status = await Permission.storage.request(); + if (status.isGranted) { + return true; + } else { + return false; + } + } else { + return false; + } + } + + static Future alertPermission(BuildContext context) async { + return showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return AlertDialog( + elevation: 8, + shadowColor: Colors.grey, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14.0), + ), + title: const Text("Storage Permission"), + content: const Text( + "Allow the permission to access photos/files from device storage."), + contentPadding: const EdgeInsets.symmetric(horizontal: 16.0) + .copyWith(bottom: 0), + actionsPadding: const EdgeInsets.symmetric(vertical: 0), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text("Cancel")), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + openAppSettings(); + }, + child: const Text("Allow")) + ], + ); + }); + } + + static Future titleDialog(TextEditingController textController, + BuildContext context, FocusScopeNode textFocus, String id) async { + return await showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) { + return SizedBox( + height: Get.height * 0.2, + child: AlertDialog( + titlePadding: + const EdgeInsets.symmetric(vertical: 12.0, horizontal: 18.0), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20.0)), + ), + title: const Text('Prompt Box Tile'), + contentPadding: + const EdgeInsets.symmetric(vertical: 0.0, horizontal: 18.0), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + "Enter the new name of the PromptBox.", + style: TextStyle( + fontFamily: 'Cera', fontWeight: FontWeight.w500), + ), + TextField( + controller: textController, + focusNode: textFocus, + textInputAction: TextInputAction.done, + textCapitalization: TextCapitalization.sentences, + autofocus: true, + maxLength: 26, + cursorColor: Theme.of(context).primaryColor, + decoration: InputDecoration( + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).primaryColor)), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).primaryColor)), + hintText: "Name...", + hintStyle: TextStyle( + color: Theme.of(context).primaryColor, + fontFamily: "Cera")), + ) + ], + ), + actions: [ + TextButton( + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).primaryColor), + child: const Text('Cancel'), + onPressed: () { + textController.clear(); + Navigator.of(context).pop(); + }, + ), + TextButton( + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).primaryColor), + child: const Text('Submit'), + onPressed: () { + if (textController.text.isNotEmpty) { + Get.find().changeChatBoxTitle( + newChatTitle: textController.text, chatId: id); + textController.clear(); + Navigator.of(context).pop(); + } + }, + ), + ], ), - ], - snackPosition: SnackPosition.BOTTOM, - borderRadius: 20, - margin: const EdgeInsets.all(12), - isDismissible: true, - dismissDirection: DismissDirection.horizontal, - forwardAnimationCurve: Curves.easeOutBack, - icon: const Icon( - Icons.error, - color: Colors.white, - ), - borderColor: Colors.white, - borderWidth: 2)); + ); + }, + ); + } + + static Future deleteDialog( + BuildContext context, String promptId) async { + return await showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) { + return SizedBox( + height: Get.height * 0.2, + child: AlertDialog( + titlePadding: + const EdgeInsets.symmetric(vertical: 12.0, horizontal: 18.0), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20.0)), + ), + title: const Text('Delete Prompt Box'), + contentPadding: + const EdgeInsets.symmetric(vertical: 0.0, horizontal: 18.0), + content: const Text( + "Are you sure you want to delete this PromptBox permanently.", + style: TextStyle(fontFamily: 'Cera', fontWeight: FontWeight.w500), + ), + actions: [ + TextButton( + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).primaryColor), + child: const Text('NO'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).primaryColor), + child: const Text('YES'), + onPressed: () { + Get.find().deleteChatBox(chatId: promptId); + Navigator.of(context).pop(); + }), + ], + ), + ); + }, + ); } } diff --git a/lib/utils/config.dart b/lib/utils/config.dart new file mode 100644 index 0000000..40a9306 --- /dev/null +++ b/lib/utils/config.dart @@ -0,0 +1,6 @@ +class Config { + static const String geminiKey = 'AIzaSyAaVcLjHcOpg-K0K9QpnHJE0s1uYEEujFQ'; + static const String geminiContentUrl = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key='; + static const String imagineUrl = "https://api.vyro.ai/v2/image/generations"; + static const String imagineKey = "vk-BfBJLcTo17xx2QJQeouR3Ulcp6avktxizZ2FUHDD4kUPJ4N"; +} diff --git a/lib/widgets/feature_box.dart b/lib/widgets/feature_box.dart index 04f7d2b..7bed8bb 100644 --- a/lib/widgets/feature_box.dart +++ b/lib/widgets/feature_box.dart @@ -32,6 +32,7 @@ class FeatureBox extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(vertical: 14.0, horizontal: 10.0), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Align( alignment: Alignment.centerLeft, @@ -46,15 +47,12 @@ class FeatureBox extends StatelessWidget { ), ), const SizedBox(height: 3), - Padding( - padding: const EdgeInsets.only(right: 20), - child: Text( - descriptionText, - style: const TextStyle( - fontFamily: 'Cera', - color: Colors.black54, - fontStyle: FontStyle.italic), - ), + Text( + descriptionText, + style: const TextStyle( + fontFamily: 'Cera', + color: Colors.black54, + fontStyle: FontStyle.italic), ), ], ), diff --git a/lib/widgets/image_gridview.dart b/lib/widgets/image_gridview.dart new file mode 100644 index 0000000..cab696b --- /dev/null +++ b/lib/widgets/image_gridview.dart @@ -0,0 +1,42 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +class ImageGridView extends StatelessWidget { + const ImageGridView({super.key, required this.images}); + final List images; + + @override + Widget build(BuildContext context) { + final height = MediaQuery.of(context).size.height; + if (images.length == 1) { + return ClipRRect( + borderRadius: BorderRadius.circular(16.0), + child: Image.file( + fit: BoxFit.cover, + height: height * 0.125, + File(images[0]), + ), + ); + } else { + return GridView.count( + shrinkWrap: true, + mainAxisSpacing: 6.0, + crossAxisSpacing: 6.0, + crossAxisCount: (images.length % 2 == 0 || images.length == 3) ? 2 : 3, + children: images + .map( + (image) => ClipRRect( + borderRadius: BorderRadius.circular(10.0), + child: Image.file( + height: height * 0.25, + fit: BoxFit.cover, + File(image), + ), + ), + ) + .toList(), + ); + } + } +} diff --git a/lib/widgets/image_prompt.dart b/lib/widgets/image_prompt.dart new file mode 100644 index 0000000..b780e49 --- /dev/null +++ b/lib/widgets/image_prompt.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:voice_assistant/widgets/preview_images.dart'; +import 'package:voice_assistant/widgets/prompt_container.dart'; + +import '../presentation/controllers/home_controller.dart'; +import '../utils/alert_messages.dart'; + +class ImagePrompt extends StatefulWidget { + const ImagePrompt({super.key}); + + @override + State createState() => _ImagePromptState(); +} + +class _ImagePromptState extends State with WidgetsBindingObserver { + final TextEditingController editController = TextEditingController(); + final FocusScopeNode textFieldFocus = FocusScopeNode(); + final ctrl = Get.find(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + ctrl.imagesFileList.clear(); + ctrl.filePath.value = ""; + WidgetsBinding.instance.removeObserver(this); + editController.dispose(); + textFieldFocus.dispose(); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + if (editController.text.isEmpty) { + textFieldFocus.requestScopeFocus(); + } + } + } + + @override + Widget build(BuildContext context) { + return PromptContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const PreviewImages(), + TextFormField( + controller: editController, + showCursor: true, + focusNode: textFieldFocus, + maxLines: null, + cursorColor: Colors.black45, + textInputAction: TextInputAction.done, + textCapitalization: TextCapitalization.sentences, + canRequestFocus: true, + decoration: InputDecoration( + border: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.black45, width: 2)), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.black45, width: 2)), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.black45, width: 2)), + contentPadding: + const EdgeInsets.symmetric(horizontal: 4.0, vertical: 0.0), + hintText: "prompt...", + filled: true, + fillColor: Colors.grey.shade200, + hintStyle: const TextStyle( + fontFamily: 'Cera', color: Colors.black54, fontSize: 16), + suffix: InkWell( + onTap: () { + (ctrl.imagesFileList.isEmpty && ctrl.filePath.isEmpty) + ? AlertMessages.showSnackBar( + "Add at least one image/file.") + : (editController.text.isEmpty) + ? AlertMessages.showSnackBar( + "write the prompt for images/files") + : (ctrl.imagesFileList.isNotEmpty) + ? ctrl.sendPrompt(editController.text) + : ctrl.uploadPdf(editController.text); + editController.clear(); + }, + child: const Icon(Icons.send_rounded))), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/multiple_floating.dart b/lib/widgets/multiple_floating.dart new file mode 100644 index 0000000..d4c92c4 --- /dev/null +++ b/lib/widgets/multiple_floating.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; +import 'package:get/get.dart'; +import 'package:voice_assistant/presentation/controllers/home_controller.dart'; + +class MultipleFloating extends StatelessWidget { + MultipleFloating({super.key, required this.fabKey}); + + final ctrl = Get.find(); + final GlobalKey fabKey; + + @override + Widget build(BuildContext context) { + return Obx(() => (ctrl.isTextPrompt.value == false) + ? FloatingActionButton( + heroTag: UniqueKey(), + onPressed: () async { + if (await ctrl.speech.hasPermission && + ctrl.speech.isNotListening) { + await ctrl.startListening(); + } else if (ctrl.speech.isListening) { + await ctrl.stopListening(); + } else { + ctrl.initialize(); + } + }, + shape: const CircleBorder( + side: BorderSide(color: Colors.white, width: 2)), + backgroundColor: Theme.of(context).primaryColor, + child: Obx( + () => Icon( + (ctrl.speechListen.value) ? Icons.stop : Icons.mic, + color: Colors.white, + ), + ), + ) + : ExpandableFab( + key: fabKey, + fanAngle: 120, + distance: 60, + openButtonBuilder: RotateFloatingActionButtonBuilder( + child: const Icon(Icons.menu_open_rounded), + fabSize: ExpandableFabSize.regular, + foregroundColor: Colors.white, + backgroundColor: Theme.of(context).primaryColor, + shape: const CircleBorder(), + ), + closeButtonBuilder: RotateFloatingActionButtonBuilder( + child: InkWell( + onTap: () { + final state = fabKey.currentState; + if (state != null) { + state.toggle(); + } + }, + child: const Icon(Icons.close)), + fabSize: ExpandableFabSize.small, + foregroundColor: Colors.white, + backgroundColor: Theme.of(context).primaryColor, + shape: const CircleBorder(), + ), + children: [ + FloatingActionButton( + heroTag: UniqueKey(), + onPressed: () async { + if (await ctrl.speech.hasPermission && + ctrl.speech.isNotListening) { + await ctrl.startListening(); + } else if (ctrl.speech.isListening) { + await ctrl.stopListening(); + } else { + ctrl.initialize(); + } + }, + shape: const CircleBorder( + side: BorderSide(color: Colors.white, width: 2)), + backgroundColor: Theme.of(context).primaryColor, + child: Obx( + () => Icon( + (ctrl.speechListen.value) ? Icons.stop : Icons.mic, + color: Colors.white, + ), + ), + ), + FloatingActionButton( + heroTag: UniqueKey(), + onPressed: () { + (ctrl.isStopped.value) ? ctrl.playTTs() : ctrl.stopTTs(); + }, + shape: const CircleBorder( + side: BorderSide(color: Colors.white, width: 2)), + backgroundColor: Theme.of(context).primaryColor, + child: Icon( + (ctrl.isStopped.value) ? Icons.play_arrow : Icons.stop, + color: Colors.white, + ), + ), + FloatingActionButton( + heroTag: UniqueKey(), + onPressed: () { + final state = fabKey.currentState; + if (state != null) { + state.toggle(); + ctrl.resetAll(); + } + }, + shape: const CircleBorder( + side: BorderSide(color: Colors.white, width: 2)), + backgroundColor: Theme.of(context).primaryColor, + child: const Icon( + Icons.restart_alt_rounded, + color: Colors.white, + ), + ) + ], + )); + } +} diff --git a/lib/widgets/preview_images.dart b/lib/widgets/preview_images.dart new file mode 100644 index 0000000..ce9f67f --- /dev/null +++ b/lib/widgets/preview_images.dart @@ -0,0 +1,153 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shimmer/shimmer.dart'; + +import '../presentation/controllers/home_controller.dart'; + +class PreviewImages extends StatelessWidget { + const PreviewImages({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + final ctrl = Get.find(); + + return Obx(() => (ctrl.imagesFileList.isEmpty && ctrl.filePath.isEmpty) + ? SizedBox( + height: width * 0.2, + width: double.infinity, + child: Shimmer.fromColors( + baseColor: Colors.grey.shade200, + highlightColor: Colors.grey.shade400, + child: ListView.builder( + itemBuilder: (context, index) { + return Container( + height: width * 0.2, + width: width * 0.2, + margin: const EdgeInsets.only(right: 12.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.0), + )); + }, + itemCount: 4, + scrollDirection: Axis.horizontal, + )), + ) + : (ctrl.imagesFileList.isNotEmpty) + ? SizedBox( + height: width * 0.225, + width: double.infinity, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.only(right: 4.0), + itemCount: ctrl.imagesFileList.length, + itemBuilder: (context, index) { + return Stack(children: [ + Container( + padding: const EdgeInsets.all(2.0), + margin: const EdgeInsets.only(right: 6.0), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16.0), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade300, + Colors.lightGreenAccent.shade100 + ], + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16.0), + child: Image.file( + File(ctrl.imagesFileList[index]), + height: width * 0.225, + width: width * 0.225, + fit: BoxFit.cover, + ))), + positionWidget(ctrl, index) + ]); + })) + : Stack(children: [ + Container( + height: width * 0.15, + margin: const EdgeInsets.all(3.0), + decoration: BoxDecoration( + color: Colors.yellow, + borderRadius: BorderRadius.circular(12.0), + boxShadow: [ + BoxShadow( + color: Colors.blue.shade300, + blurRadius: 4.0, + spreadRadius: 1.0), + BoxShadow( + color: Colors.lightGreenAccent.shade100, + blurRadius: 4.0, + spreadRadius: 0.5) + ], + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade300, + Colors.lightGreenAccent.shade100 + ], + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: width * 0.01, + ), + Icon( + Icons.picture_as_pdf, + color: Colors.red.shade300, + size: 40, + ), + Flexible( + child: Text( + ctrl.filePath.value.split('/').last.split('-').last, + style: const TextStyle( + color: Colors.white, + fontFamily: "Cera", + fontSize: 16), + ), + ), + SizedBox( + width: width * 0.01, + ), + ], + ), + ), + positionWidget(ctrl, null) + ])); + } + + Widget positionWidget(HomeController ctrl, int? index) { + return Positioned( + top: 0, + right: (index == null) ? 0 : 6, + child: InkWell( + onTap: () { + (ctrl.imagesFileList.isNotEmpty) + ? ctrl.imagesFileList.removeAt(index!) + : ctrl.filePath.value = ""; + }, + child: const CircleAvatar( + radius: 8, + backgroundColor: Colors.black45, + child: Center( + child: Icon(Icons.clear, color: Colors.white, size: 10), + )), + )); + } +} diff --git a/lib/widgets/prompt_container.dart b/lib/widgets/prompt_container.dart new file mode 100644 index 0000000..db1688e --- /dev/null +++ b/lib/widgets/prompt_container.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class PromptContainer extends StatelessWidget { + const PromptContainer({super.key, required this.child}); + + final dynamic child; + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + final height = MediaQuery.of(context).size.height; + + return Container( + width: width, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + margin: EdgeInsets.symmetric( + horizontal: width * 0.04, vertical: height * 0.02), + decoration: BoxDecoration( + border: Border.all(color: Colors.blueGrey.shade600, width: 1.25), + borderRadius: BorderRadius.circular(24) + .copyWith(topLeft: Radius.zero, bottomRight: Radius.zero), + ), + child: child, + ); + } +} diff --git a/lib/widgets/prompt_messages.dart b/lib/widgets/prompt_messages.dart new file mode 100644 index 0000000..9133845 --- /dev/null +++ b/lib/widgets/prompt_messages.dart @@ -0,0 +1,191 @@ +import 'package:animated_text_kit/animated_text_kit.dart'; +import 'package:flutter/material.dart'; +import 'package:voice_assistant/data/adapters/models_adapter.dart'; + +import '../presentation/controllers/home_controller.dart'; +import 'image_gridview.dart'; + +class PromptMessagesWidget extends StatelessWidget { + const PromptMessagesWidget( + {super.key, required this.ctrl, required this.message}); + + final List message; + final HomeController ctrl; + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: message.asMap().entries.map((entry) { + final index = entry.key; + final msg = entry.value; + + // Check if it's the last message + final isLastMessage = index == message.length - 1; + + if (msg.isUser && (msg.imagePath != null || msg.filePath != null)) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: Alignment.centerRight, + child: Container( + width: (msg.imagePath != null && msg.imagePath!.length == 1) + ? width * 0.25 + : (msg.filePath != null) + ? width * 0.25 + : width * 0.5, + margin: const EdgeInsets.only(bottom: 12.0), + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Colors.grey, + spreadRadius: 1.0, + blurRadius: 6.0, + ), + ], + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade300, + Colors.lightGreenAccent.shade100 + ], + ), + borderRadius: BorderRadius.circular(24).copyWith( + topLeft: Radius.zero, bottomRight: Radius.zero), + ), + child: (msg.imagePath != null) + ? ImageGridView(images: msg.imagePath!) + : Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: width * 0.015, + ), + Icon( + Icons.picture_as_pdf, + color: Colors.red.shade300, + size: 30, + ), + SizedBox( + width: width * 0.015, + ), + Flexible( + child: Text( + ctrl.filePath.value + .split('/') + .last + .split('-') + .last, + style: const TextStyle( + color: Colors.white, fontFamily: "Cera"), + ), + ), + ], + )), + ), + Text( + "Prompt: ${msg.text}", + softWrap: true, + style: const TextStyle( + fontFamily: 'Cera', + color: Colors.black87, + fontSize: 16, + ), + ), + ], + ); + } else { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + (isLastMessage && ctrl.shouldTextAnimate.value) + ? AnimatedTextKit( + onFinished: () { + ctrl.shouldTextAnimate.value = false; + }, + onTap: () { + ctrl.shouldTextAnimate.value = false; + }, + displayFullTextOnTap: true, + animatedTexts: [ + TyperAnimatedText( + speed: const Duration(milliseconds: 55), + msg.isUser + ? "Prompt: ${msg.text}" + : "Response: ${msg.text}", + textStyle: TextStyle( + fontFamily: 'Cera', + color: msg.isUser + ? Colors.black87 + : (msg.text == "Failed") + ? Colors.red + : Colors.grey, + fontSize: 16, + ), + textAlign: (!msg.isUser && msg.text == "Failed") + ? TextAlign.end + : TextAlign.start, + ), + ], + isRepeatingAnimation: false, + ) + : Text( + msg.isUser + ? "Prompt: ${msg.text}" + : "Response: ${msg.text}", + style: TextStyle( + fontFamily: 'Cera', + color: msg.isUser + ? Colors.black87 + : (msg.text == "Failed") + ? Colors.red + : Colors.grey, + fontSize: 16, + ), + textAlign: (!msg.isUser && msg.text == "Failed") + ? TextAlign.end + : TextAlign.start, + ), + if (msg.imagePath != null) + Align( + alignment: Alignment.centerRight, + child: Container( + width: width * 0.5, + margin: const EdgeInsets.only(bottom: 12.0), + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: Colors.grey, + spreadRadius: 1.0, + blurRadius: 6.0, + ), + ], + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade300, + Colors.lightGreenAccent.shade100 + ], + ), + borderRadius: BorderRadius.circular(24).copyWith( + topLeft: Radius.zero, bottomRight: Radius.zero), + ), + child: ImageGridView(images: msg.imagePath!))) + ], + ); + } + }).toList(), + ); + } +} diff --git a/lib/widgets/virtual_assistant_image.dart b/lib/widgets/virtual_assistant_image.dart new file mode 100644 index 0000000..b0eac4a --- /dev/null +++ b/lib/widgets/virtual_assistant_image.dart @@ -0,0 +1,50 @@ +import 'package:animate_do/animate_do.dart'; +import 'package:flutter/material.dart'; + +class VirtualAssistantImage extends StatelessWidget { + const VirtualAssistantImage({super.key}); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + + return ZoomIn( + duration: const Duration(milliseconds: 600), + delay: const Duration(milliseconds: 200), + child: Stack( + children: [ + Center( + child: Container( + height: width * 0.35, + width: width * 0.35, + margin: const EdgeInsets.only(top: 4), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 5), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.shade300, + Colors.lightGreenAccent.shade100 + ], + ), + ), + ), + ), + Container( + height: width * 0.35, + decoration: const BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + image: AssetImage( + 'assets/images/virtualAssistant.png', + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 0ccfc26..0a640ab 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,27 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + url: "https://pub.dev" + source: hosted + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + url: "https://pub.dev" + source: hosted + version: "6.7.0" animate_do: dependency: "direct main" description: @@ -17,6 +38,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.2" + args: + dependency: transitive + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" + source: hosted + version: "2.6.0" async: dependency: transitive description: @@ -33,6 +62,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct main" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.dev" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" characters: dependency: transitive description: @@ -41,6 +134,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" clock: dependency: transitive description: @@ -49,6 +150,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" collection: dependency: transitive description: @@ -57,6 +166,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -65,6 +198,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + url: "https://pub.dev" + source: hosted + version: "2.3.7" + dio: + dependency: transitive + description: + name: dio + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + url: "https://pub.dev" + source: hosted + version: "2.0.0" fake_async: dependency: transitive description: @@ -73,6 +230,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: aac85f20436608e01a6ffd1fdd4e746a7f33c93a2c83752e626bdfaea139b877 + url: "https://pub.dev" + source: hosted + version: "8.1.3" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -86,6 +307,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + flutter_gemini: + dependency: "direct main" + description: + name: flutter_gemini + sha256: b7264b1d19acc4b1a5628a0e26c0976aa1fb948f0d3243bc3510ff51e09476b7 + url: "https://pub.dev" + source: hosted + version: "3.0.0" flutter_lints: dependency: "direct dev" description: @@ -94,6 +323,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" + url: "https://pub.dev" + source: hosted + version: "2.0.23" flutter_test: dependency: "direct dev" description: flutter @@ -112,6 +349,14 @@ packages: description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" get: dependency: "direct main" description: @@ -120,6 +365,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.6.6" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" google_generative_ai: dependency: "direct main" description: @@ -128,6 +381,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.6" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" + hive_generator: + dependency: "direct main" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.1" http: dependency: "direct main" description: @@ -136,6 +421,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" http_parser: dependency: transitive description: @@ -144,6 +437,86 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: fa8141602fde3f7e2f81dbf043613eb44dfa325fa0bcf93c0f142c9f7a2c193e + url: "https://pub.dev" + source: hosted + version: "0.8.12+18" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b" + url: "https://pub.dev" + source: hosted + version: "0.8.12+1" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" json_annotation: dependency: transitive description: @@ -192,6 +565,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -216,6 +605,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + url: "https://pub.dev" + source: hosted + version: "2.1.1" path: dependency: transitive description: @@ -224,6 +629,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + url: "https://pub.dev" + source: hosted + version: "2.2.15" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + pdf_gemini: + dependency: "direct main" + description: + name: pdf_gemini + sha256: eaf4b0ecc34db4785efafe5452459e0b036f83bb60b33dd07de8215bb6e071c3 + url: "https://pub.dev" + source: hosted + version: "0.0.3" pedantic: dependency: transitive description: @@ -280,6 +741,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -288,11 +757,75 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" source_span: dependency: transitive description: @@ -333,6 +866,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" string_scanner: dependency: transitive description: @@ -357,6 +898,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" typed_data: dependency: transitive description: @@ -381,6 +930,14 @@ packages: url: "https://pub.dev" source: hosted version: "14.2.5" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" web: dependency: transitive description: @@ -389,6 +946,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" + url: "https://pub.dev" + source: hosted + version: "5.9.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" sdks: dart: ">=3.5.3 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 82358fd..dc0c9c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,14 +36,24 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. animate_do: ^3.3.4 animated_text_kit: ^4.2.2 + build_runner: ^2.4.0 cupertino_icons: ^1.0.8 + file_picker: ^8.1.3 flutter_tts: ^4.0.2 flutter_expandable_fab: ^2.3.0 + flutter_gemini: ^3.0.0 get: ^4.6.6 google_generative_ai: ^0.4.6 + hive: ^2.2.3 + hive_flutter: ^1.1.0 + hive_generator: ^2.0.0 http: ^1.2.2 + image_picker: ^1.1.2 loading_animation_widget: ^1.3.0 + path_provider: ^2.1.5 + pdf_gemini: ^0.0.3 permission_handler: ^11.3.1 + shimmer: ^3.0.0 speech_to_text: ^7.0.0 dev_dependencies: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index bf3b1fe..0ab9fc1 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,10 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterTtsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterTtsPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 004bc8c..1771c8f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows flutter_tts permission_handler_windows )