diff --git a/android/app/build.gradle b/android/app/build.gradle index b7fd22a..e8a1436 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -45,7 +45,8 @@ android { applicationId "com.vinaybyte.convogen" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion + // minSdkVersion flutter.minSdkVersion + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b44696f..f75d48f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,11 @@ + + + + + - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Convogen - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - convogen - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Convogen + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + convogen + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + NSSpeechRecognitionUsageDescription + Speech recognition is used to search text over google's gemini api + NSMicrophoneUsageDescription + Speech recognition is used to search text over google's gemini api + + \ No newline at end of file diff --git a/lib/providers/gemini_chat_provider.dart b/lib/providers/gemini_chat_provider.dart index b8c29d2..7d1c786 100644 --- a/lib/providers/gemini_chat_provider.dart +++ b/lib/providers/gemini_chat_provider.dart @@ -52,6 +52,10 @@ class GeminiChatProvider extends StateNotifier { state = state.copyWith(isTyping: isTyping); } + reset() { + state = state.copyWith(isLoading: false, messages: [], isTyping: false); + } + init() async { // create a timeout await Future.delayed(const Duration(seconds: 2)); diff --git a/lib/screens/chat.dart b/lib/screens/chat.dart index 072fe24..2c78f79 100644 --- a/lib/screens/chat.dart +++ b/lib/screens/chat.dart @@ -1,7 +1,10 @@ import 'dart:developer'; import 'dart:io'; +import 'package:flutter/cupertino.dart'; import "package:flutter/material.dart"; +import 'package:flutter/services.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:image_picker/image_picker.dart'; import 'package:loading_animation_widget/loading_animation_widget.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart'; @@ -9,6 +12,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:convogen/providers/gemini_chat_provider.dart'; import 'package:simple_gradient_text/simple_gradient_text.dart'; import 'package:shimmer/shimmer.dart'; +import 'package:speech_to_text/speech_recognition_result.dart'; +import 'package:speech_to_text/speech_to_text.dart' as stt; +import 'package:toastification/toastification.dart'; class ChatPage extends ConsumerStatefulWidget { const ChatPage({super.key}); @@ -35,6 +41,64 @@ class _ChatPageState extends ConsumerState { customStatusBuilder: (message, {required context}) { return const SizedBox(); }, + textMessageBuilder: (p0, {required messageWidth, required showName}) { + if (p0.author.id == '1') { + return Padding( + padding: const EdgeInsets.all(15.0), + child: Text(p0.text, style: const TextStyle(color: Colors.white)), + ); + } + return Markdown( + data: p0.text, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + ); + }, + listBottomWidget: AnimatedContainer( + height: geminiChat.isTyping ? 0 : 80, + duration: const Duration(milliseconds: 300), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0) + .copyWith(bottom: 20), + child: Row( + children: [ + TextButton( + onPressed: () { + ref.read(geminiChatProvider.notifier).reset(); + }, + child: Row( + children: [ + Icon(CupertinoIcons.bolt_circle, + color: Theme.of(context).colorScheme.primary), + const SizedBox(width: 5), + const Text("Start New Chat"), + ], + )), + IconButton( + onPressed: () { + var text = + (geminiChat.messages.first.toJson()["type"] == 'text') + ? geminiChat.messages.first.toJson()["text"] + : ''; + log(text); + Clipboard.setData(ClipboardData(text: text)); + toastification.show( + context: context, + type: ToastificationType.success, + style: ToastificationStyle.flat, + title: const Text('Copied'), + description: const Text('Copied to clipboard'), + alignment: Alignment.bottomCenter, + autoCloseDuration: const Duration(seconds: 4), + boxShadow: lowModeShadow, + ); + }, + icon: Icon(Icons.copy_rounded, + color: Theme.of(context).colorScheme.primary)) + ], + ), + ), + ), emptyState: EmptyStateWidget(onSendPressed: (p0) async { FocusManager.instance.primaryFocus?.unfocus(); await ref @@ -79,6 +143,7 @@ class _ChatPageState extends ConsumerState { .read(geminiChatProvider.notifier) .getPrompt(p0, selectedImage); } + log("SEND PRESSED: $p0"); }), typingIndicatorOptions: TypingIndicatorOptions( customTypingIndicator: Padding( @@ -230,7 +295,7 @@ class EmptyStateWidget extends StatelessWidget { } } -class CustomBottomInputBar extends StatelessWidget { +class CustomBottomInputBar extends StatefulWidget { final bool collapsed; final Function onSendPressed; final Function setImage; @@ -242,16 +307,72 @@ class CustomBottomInputBar extends StatelessWidget { required this.onSendPressed, required this.setImage}); + @override + State createState() => _CustomBottomInputBarState(); +} + +class _CustomBottomInputBarState extends State { + var inputController = TextEditingController(); + final stt.SpeechToText _speechToText = stt.SpeechToText(); + bool _speechEnabled = false; + String _lastWords = ''; + + @override + void initState() { + super.initState(); + _initSpeech(); + } + + /// This has to happen only once per app + void _initSpeech() async { + _speechEnabled = await _speechToText.initialize(); + setState(() {}); + } + + /// Each time to start a speech recognition session + void _startListening() async { + await _speechToText.listen(onResult: _onSpeechResult); + setState(() {}); + } + + /// 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. + void _stopListening() async { + await _speechToText.stop(); + setState(() {}); + } + + /// This is the callback that the SpeechToText plugin calls when + /// the platform returns recognized words. + void _onSpeechResult(SpeechRecognitionResult result) { + setState(() { + _lastWords = result.recognizedWords; + if (result.finalResult) { + inputController.text = _lastWords; + } + }); + } + @override Widget build(BuildContext context) { - var inputController = TextEditingController(); + handleMicPress() async { + log("Mic Pressed"); + if (_speechEnabled) { + _startListening(); + } else { + // show snackbar + log("Speech not enabled"); + } + } handleCameraPressed() { log("Camera pressed"); ImagePicker().pickImage(source: ImageSource.gallery).then((image) { if (image != null) { log("IMAGE SELECTED: ${image.path}"); - setImage(image); + widget.setImage(image); } }); } @@ -274,18 +395,25 @@ class CustomBottomInputBar extends StatelessWidget { children: [ TextField( onSubmitted: (value) { - onSendPressed(inputController.text); + widget.onSendPressed(inputController.text); + inputController.clear(); }, + minLines: 1, + maxLines: 5, controller: inputController, + keyboardType: TextInputType.text, decoration: InputDecoration( hintStyle: const TextStyle( fontSize: 18, ), - hintText: 'Type, talk, or share \na photo', + hintText: _speechToText.isListening + ? 'Listening...' + : 'Type, talk, or share \na photo', hintMaxLines: 2, suffix: IconButton( onPressed: () { - onSendPressed(inputController.text); + widget.onSendPressed(inputController.text); + inputController.clear(); }, icon: const Icon(Icons.send_rounded)), border: InputBorder.none)), @@ -295,28 +423,30 @@ class CustomBottomInputBar extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - selectedImage != null + widget.selectedImage != null ? Container( margin: const EdgeInsets.only(right: 10), child: ClipRRect( borderRadius: BorderRadius.circular(10), child: Image.file( - File(selectedImage!.path), + File(widget.selectedImage!.path), width: 50, height: 50, ), ), ) : const SizedBox(), - selectedImage != null + widget.selectedImage != null ? IconButton( - onPressed: () => setImage(null), + onPressed: () => widget.setImage(null), icon: Icon( Icons.delete_outline, color: Theme.of(context).colorScheme.error, )) : const SizedBox(), - selectedImage != null ? const Spacer() : const SizedBox(), + widget.selectedImage != null + ? const Spacer() + : const SizedBox(), FilledButton( // color: Colors.red, style: ButtonStyle( @@ -327,9 +457,15 @@ class CustomBottomInputBar extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - IconButton( - onPressed: () {}, - icon: const Icon(Icons.mic_none_outlined)), + _speechToText.isListening + ? IconButton( + onPressed: () async { + _stopListening(); + }, + icon: const Icon(Icons.stop_circle_outlined)) + : IconButton( + onPressed: handleMicPress, + icon: const Icon(Icons.mic_none_outlined)), const SizedBox( width: 10, ), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d993eb7..34b495c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,11 +8,13 @@ import Foundation import file_selector_macos import path_provider_foundation import shared_preferences_foundation +import speech_to_text_macos import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SpeechToTextMacosPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextMacosPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index a995465..b8e8ba4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -427,6 +427,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + sha256: "9921f9deda326f8a885e202b1e35237eadfc1345239a0f6f0f1ff287e047547f" + url: "https://pub.dev" + source: hosted + version: "0.7.1" flutter_parsed_text: dependency: transitive description: @@ -733,6 +741,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.7.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 + url: "https://pub.dev" + source: hosted + version: "7.2.2" matcher: dependency: transitive description: @@ -853,6 +869,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.0+3" + pedantic: + dependency: transitive + description: + name: pedantic + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" + source: hosted + version: "1.11.1" petitparser: dependency: transitive description: @@ -1090,6 +1114,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + speech_to_text: + dependency: "direct main" + description: + name: speech_to_text + sha256: "771686424c8b065b814823d63245c593f8a6f066537d658c837bfe51887ac4ce" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + speech_to_text_macos: + dependency: transitive + description: + name: speech_to_text_macos + sha256: e685750f7542fcaa087a5396ee471e727ec648bf681f4da83c84d086322173f6 + url: "https://pub.dev" + source: hosted + version: "1.1.0" + speech_to_text_platform_interface: + dependency: transitive + description: + name: speech_to_text_platform_interface + sha256: a0df1a907091ea09880077dc25aae02af9f79811264e6e97ddb08639b7f771c2 + url: "https://pub.dev" + source: hosted + version: "2.2.0" sprintf: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4ae2e07..1302d5e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.1.0 +version: 1.2.0 environment: sdk: ">=3.2.3 <4.0.0" @@ -53,6 +53,8 @@ dependencies: shimmer: ^3.0.0 image_picker: ^1.1.0 google_generative_ai: ^0.3.1 + speech_to_text: ^6.6.1 + flutter_markdown: ^0.7.1 dev_dependencies: flutter_test: