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