diff --git a/Frontend/lib/mih_package_components/mih_dropdwn_field.dart b/Frontend/lib/mih_package_components/mih_dropdwn_field.dart index 9c0d6154..dd8d46ee 100644 --- a/Frontend/lib/mih_package_components/mih_dropdwn_field.dart +++ b/Frontend/lib/mih_package_components/mih_dropdwn_field.dart @@ -98,131 +98,166 @@ class _MihDropdownFieldState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Theme( - data: Theme.of(context).copyWith( - textSelectionTheme: TextSelectionThemeData( - cursorColor: MihColors.getPrimaryColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark"), - selectionColor: MihColors.getPrimaryColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark") - .withValues(alpha: 0.3), - selectionHandleColor: MihColors.getPrimaryColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark"), + Row( + children: [ + Expanded( + child: Theme( + data: Theme.of(context).copyWith( + textSelectionTheme: TextSelectionThemeData( + cursorColor: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + selectionColor: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)! + .theme + .mode == + "Dark") + .withValues(alpha: 0.3), + selectionHandleColor: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + ), + ), + child: DropdownMenu( + controller: widget.controller, + dropdownMenuEntries: menu, + enableSearch: widget.enableSearch, + enableFilter: widget.enableSearch, + enabled: widget.editable, + textInputAction: widget.enableSearch + ? TextInputAction.search + : TextInputAction.none, + requestFocusOnTap: widget.enableSearch, + menuHeight: 400, + expandedInsets: EdgeInsets.zero, + textStyle: TextStyle( + color: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + fontWeight: FontWeight.w500, + ), + trailingIcon: Icon( + Icons.arrow_drop_down, + color: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + ), + selectedTrailingIcon: Icon( + Icons.arrow_drop_up, + color: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + ), + // leadingIcon: + // IconButton( + // onPressed: () { + // widget.controller.clear(); + // field.didChange(''); + // }, + // icon: Icon( + // Icons.delete_outline_rounded, + // color: MihColors.getPrimaryColor( + // MzansiInnovationHub.of(context)!.theme.mode == + // "Dark"), + // ), + // ), + onSelected: (String? selectedValue) { + field.didChange(selectedValue); + widget.onSelected?.call(selectedValue); + }, + menuStyle: MenuStyle( + backgroundColor: WidgetStatePropertyAll( + MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)! + .theme + .mode == + "Dark")), + side: WidgetStatePropertyAll( + BorderSide( + color: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)! + .theme + .mode == + "Dark"), + width: 1.0), + ), + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 10), // Increase for more roundness + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + errorStyle: const TextStyle(height: 0, fontSize: 0), + contentPadding: const EdgeInsets.symmetric( + horizontal: 10.0, vertical: 8.0), + filled: true, + fillColor: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == + "Dark"), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide( + color: field.hasError + ? MihColors.getRedColor( + MzansiInnovationHub.of(context)! + .theme + .mode == + "Dark") + : MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)! + .theme + .mode == + "Dark"), + width: 3.0, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide( + color: MihColors.getRedColor( + MzansiInnovationHub.of(context)! + .theme + .mode == + "Dark"), + width: 3.0, + ), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide( + color: MihColors.getRedColor( + MzansiInnovationHub.of(context)! + .theme + .mode == + "Dark"), + width: 3.0, + ), + ), + ), + ), + ), ), - ), - child: DropdownMenu( - controller: widget.controller, - dropdownMenuEntries: menu, - enableSearch: widget.enableSearch, - enableFilter: widget.enableSearch, - enabled: widget.editable, - textInputAction: widget.enableSearch - ? TextInputAction.search - : TextInputAction.none, - requestFocusOnTap: widget.enableSearch, - menuHeight: 400, - expandedInsets: EdgeInsets.zero, - textStyle: TextStyle( - color: MihColors.getPrimaryColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark"), - fontWeight: FontWeight.w500, - ), - trailingIcon: Icon( - Icons.arrow_drop_down, - color: MihColors.getPrimaryColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark"), - ), - selectedTrailingIcon: Icon( - Icons.arrow_drop_up, - color: MihColors.getPrimaryColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark"), - ), - leadingIcon: IconButton( - onPressed: () { + const SizedBox(width: 8), + GestureDetector( + onTap: () { widget.controller.clear(); field.didChange(''); }, - icon: Icon( - Icons.delete_outline_rounded, - color: MihColors.getPrimaryColor( + child: Icon( + size: 35, + Icons.delete_rounded, + color: MihColors.getSecondaryColor( MzansiInnovationHub.of(context)!.theme.mode == "Dark"), ), ), - onSelected: (String? selectedValue) { - field.didChange(selectedValue); - widget.onSelected?.call(selectedValue); - }, - menuStyle: MenuStyle( - backgroundColor: WidgetStatePropertyAll( - MihColors.getSecondaryColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark")), - side: WidgetStatePropertyAll( - BorderSide( - color: MihColors.getPrimaryColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark"), - width: 1.0), - ), - shape: WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 10), // Increase for more roundness - ), - ), - ), - inputDecorationTheme: InputDecorationTheme( - errorStyle: const TextStyle(height: 0, fontSize: 0), - contentPadding: const EdgeInsets.symmetric( - horizontal: 10.0, vertical: 8.0), - filled: true, - fillColor: MihColors.getSecondaryColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark"), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.0), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.0), - borderSide: BorderSide( - color: field.hasError - ? MihColors.getRedColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark") - : MihColors.getSecondaryColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark"), - width: 3.0, - ), - ), - errorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.0), - borderSide: BorderSide( - color: MihColors.getRedColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark"), - width: 3.0, - ), - ), - focusedErrorBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.0), - borderSide: BorderSide( - color: MihColors.getRedColor( - MzansiInnovationHub.of(context)!.theme.mode == - "Dark"), - width: 3.0, - ), - ), - ), - ), + ], ), if (field.hasError) Padding( diff --git a/Frontend/lib/mih_packages/mzansi_ai/package_tools/mih_ai_chat.dart b/Frontend/lib/mih_packages/mzansi_ai/package_tools/mih_ai_chat.dart index e4e43085..1e87761d 100644 --- a/Frontend/lib/mih_packages/mzansi_ai/package_tools/mih_ai_chat.dart +++ b/Frontend/lib/mih_packages/mzansi_ai/package_tools/mih_ai_chat.dart @@ -228,10 +228,11 @@ class _MihAiChatState extends State with WidgetsBindingObserver { LlmChatView( provider: aiProvider.ollamaProvider, messageSender: aiProvider.ollamaProvider.sendMessageStream, + speechToText: aiProvider.ollamaProvider.speechToText, // welcomeMessage: // "Mzansi AI is here to help. Send us a messahe and we'll try our best to assist you.", autofocus: false, - enableAttachments: false, + enableAttachments: true, enableVoiceNotes: false, style: aiProvider.getChatStyle(context), suggestions: [ diff --git a/Frontend/lib/mih_providers/mzansi_ai_provider.dart b/Frontend/lib/mih_providers/mzansi_ai_provider.dart index a74520ca..2bda84a6 100644 --- a/Frontend/lib/mih_providers/mzansi_ai_provider.dart +++ b/Frontend/lib/mih_providers/mzansi_ai_provider.dart @@ -19,7 +19,10 @@ class MzansiAiProvider extends ChangeNotifier { }) { ollamaProvider = OllamaProvider( baseUrl: "${AppEnviroment.baseAiUrl}/api", - model: AppEnviroment.getEnv() == "Prod" ? 'gemma3n:e4b' : "gemma3:1b", + model: AppEnviroment.getEnv() == "Prod" + ? 'gemma3n:e4b' + : "qwen3-vl:2b-instruct", + think: false, systemPrompt: "You are Mzansi AI, a helpful and friendly AI assistant running on the 'MIH App'.\n" + "The MIH App was created by 'Mzansi Innovation Hub', a South African-based startup company." + "Your primary purpose is to assist users by answering general questions and helping with creative writing tasks or any other task a user might have for you.\n" + @@ -147,9 +150,6 @@ class MzansiAiProvider extends ChangeNotifier { MzansiInnovationHub.of(context)!.theme.mode == "Dark"), progressIndicatorColor: MihColors.getPrimaryColor( MzansiInnovationHub.of(context)!.theme.mode == "Dark"), - menuColor: Colors.black, - // MihColors.getGreenColor( - // MzansiInnovationHub.of(context)!.theme.mode == "Dark"), disabledButtonStyle: ActionButtonStyle( icon: MihIcons.mzansiAi, iconColor: MihColors.getSecondaryColor( @@ -323,6 +323,21 @@ class MzansiAiProvider extends ChangeNotifier { MzansiInnovationHub.of(context)!.theme.mode == "Dark"), ), ), + addButtonStyle: ActionButtonStyle( + iconDecoration: BoxDecoration( + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + borderRadius: BorderRadius.circular(25), + ), + iconColor: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + textStyle: TextStyle( + color: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + ), + ), + menuColor: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), ); } } diff --git a/Frontend/lib/mih_providers/ollama_provider.dart b/Frontend/lib/mih_providers/ollama_provider.dart index 13d31c69..16e6942f 100644 --- a/Frontend/lib/mih_providers/ollama_provider.dart +++ b/Frontend/lib/mih_providers/ollama_provider.dart @@ -4,14 +4,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; import 'package:ken_logger/ken_logger.dart'; import 'package:ollama_dart/ollama_dart.dart'; +import 'package:cross_file/cross_file.dart'; class OllamaProvider extends LlmProvider with ChangeNotifier { OllamaProvider({ String? baseUrl, Map? headers, - Map? queryParams, + Map? queryParams, required String model, String? systemPrompt, + bool? think, }) : _client = OllamaClient( baseUrl: baseUrl, headers: headers, @@ -19,11 +21,13 @@ class OllamaProvider extends LlmProvider with ChangeNotifier { ), _model = model, _systemPrompt = systemPrompt, + _think = think, _history = []; final OllamaClient _client; final String _model; final List _history; final String? _systemPrompt; + final bool? _think; @override Stream generateStream( @@ -36,6 +40,29 @@ class OllamaProvider extends LlmProvider with ChangeNotifier { yield* _generateStream(messages); } + Stream speechToText(XFile audioFile) async* { + KenLogger.success("Inside Custom speechToText funtion"); + // 1. Convert the XFile to the attachment format needed for the LLM. + final attachments = [await FileAttachment.fromFile(audioFile)]; + KenLogger.success("added attachment for audio file"); + + // 2. Define the transcription prompt, mirroring the logic from LlmChatView. + const prompt = + 'translate the attached audio to text; provide the result of that ' + 'translation as just the text of the translation itself. be careful to ' + 'separate the background audio from the foreground audio and only ' + 'provide the result of translating the foreground audio.'; + + KenLogger.success("Created Prompt"); + // 3. Use your existing Ollama API call to process the prompt and attachment. + // We are essentially running a new, one-off chat session for transcription. + yield* generateStream( + prompt, + attachments: attachments, + ); + KenLogger.success("done"); + } + @override Stream sendMessageStream( String prompt, { @@ -76,6 +103,7 @@ class OllamaProvider extends LlmProvider with ChangeNotifier { Stream _generateStream(List messages) async* { final allMessages = []; if (_systemPrompt != null && _systemPrompt.isNotEmpty) { + KenLogger.success("Adding system prompt to the conversation"); allMessages.add(Message( role: MessageRole.system, content: _systemPrompt, @@ -87,6 +115,7 @@ class OllamaProvider extends LlmProvider with ChangeNotifier { request: GenerateChatCompletionRequest( model: _model, messages: allMessages, + think: _think, ), ); // final stream = _client.generateChatCompletionStream( @@ -109,19 +138,32 @@ class OllamaProvider extends LlmProvider with ChangeNotifier { content: message.text ?? '', ); } - + final imageAttachments = []; + final docAttachments = []; + if (message.text != null && message.text!.isNotEmpty) { + docAttachments.add(message.text!); + } + for (final attachment in message.attachments) { + if (attachment is FileAttachment) { + final mimeType = attachment.mimeType.toLowerCase(); + if (mimeType.startsWith('image/')) { + imageAttachments.add(base64Encode(attachment.bytes)); + } else if (mimeType == 'application/pdf' || + mimeType.startsWith('text/')) { + throw LlmFailureException( + "\n\nAww, that file is a little too advanced for us right now ($mimeType)! We're still learning, but we'll get there! Please try sending us a different file type.\n\nHint: We can handle images quite well!", + ); + } + } else { + throw LlmFailureException( + 'Unsupported attachment type: $attachment', + ); + } + } return Message( role: MessageRole.user, - content: message.text ?? '', - images: [ - for (final attachment in message.attachments) - if (attachment is ImageFileAttachment) - base64Encode(attachment.bytes) - else - throw LlmFailureException( - 'Unsupported attachment type: $attachment', - ), - ], + content: docAttachments.join(' '), + images: imageAttachments, ); case MessageOrigin.llm: diff --git a/Frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/Frontend/macos/Flutter/GeneratedPluginRegistrant.swift index 25486c96..3cef27da 100644 --- a/Frontend/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/Frontend/macos/Flutter/GeneratedPluginRegistrant.swift @@ -24,7 +24,6 @@ import record_macos import screen_brightness_macos import share_plus import shared_preferences_foundation -import speech_to_text import sqflite_darwin import syncfusion_pdfviewer_macos import url_launcher_macos @@ -50,7 +49,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - SpeechToTextPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SyncfusionFlutterPdfViewerPlugin.register(with: registry.registrar(forPlugin: "SyncfusionFlutterPdfViewerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/Frontend/pubspec.lock b/Frontend/pubspec.lock index ec7cf4f8..631b8f23 100644 --- a/Frontend/pubspec.lock +++ b/Frontend/pubspec.lock @@ -322,13 +322,13 @@ packages: source: hosted version: "3.3.0" cross_file: - dependency: transitive + dependency: "direct main" description: name: cross_file - sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" url: "https://pub.dev" source: hosted - version: "0.3.4+2" + version: "0.3.5+1" crypto: dependency: transitive description: @@ -1336,14 +1336,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - pedantic: - dependency: transitive - description: - name: pedantic - sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" - url: "https://pub.dev" - source: hosted - version: "1.11.1" petitparser: dependency: transitive description: @@ -1757,30 +1749,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" - speech_to_text: - dependency: "direct main" - description: - name: speech_to_text - sha256: c07557664974afa061f221d0d4186935bea4220728ea9446702825e8b988db04 - url: "https://pub.dev" - source: hosted - version: "7.3.0" - speech_to_text_platform_interface: - dependency: transitive - description: - name: speech_to_text_platform_interface - sha256: a1935847704e41ee468aad83181ddd2423d0833abe55d769c59afca07adb5114 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - speech_to_text_windows: - dependency: transitive - description: - name: speech_to_text_windows - sha256: "2c9846d18253c7bbe059a276297ef9f27e8a2745dead32192525beb208195072" - url: "https://pub.dev" - source: hosted - version: "1.0.0+beta.8" sprintf: dependency: transitive description: diff --git a/Frontend/pubspec.yaml b/Frontend/pubspec.yaml index 05b505fc..6c502808 100644 --- a/Frontend/pubspec.yaml +++ b/Frontend/pubspec.yaml @@ -59,7 +59,7 @@ dependencies: provider: ^6.1.5+1 flutter_ai_toolkit: ^0.10.0 flutter_markdown_plus: ^1.0.5 - speech_to_text: ^7.3.0 + cross_file: ^0.3.5+1 quick_actions: ^1.1.0 dev_dependencies: diff --git a/Frontend/windows/flutter/generated_plugin_registrant.cc b/Frontend/windows/flutter/generated_plugin_registrant.cc index f08708cc..ad71ace9 100644 --- a/Frontend/windows/flutter/generated_plugin_registrant.cc +++ b/Frontend/windows/flutter/generated_plugin_registrant.cc @@ -18,7 +18,6 @@ #include #include #include -#include #include #include @@ -47,8 +46,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); - SpeechToTextWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SpeechToTextWindows")); SyncfusionPdfviewerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SyncfusionPdfviewerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/Frontend/windows/flutter/generated_plugins.cmake b/Frontend/windows/flutter/generated_plugins.cmake index 46e96ea5..57f48306 100644 --- a/Frontend/windows/flutter/generated_plugins.cmake +++ b/Frontend/windows/flutter/generated_plugins.cmake @@ -15,7 +15,6 @@ list(APPEND FLUTTER_PLUGIN_LIST record_windows screen_brightness_windows share_plus - speech_to_text_windows syncfusion_pdfviewer_windows url_launcher_windows ) diff --git a/backend/.DS_Store b/backend/.DS_Store index 17866b74..510f15ab 100644 Binary files a/backend/.DS_Store and b/backend/.DS_Store differ