From d9fb9dd7586b4898d5164c65eee1af52e6710473 Mon Sep 17 00:00:00 2001 From: Yasien Mac Mini Date: Fri, 7 Nov 2025 14:29:05 +0200 Subject: [PATCH] QOL: Mzansi AI Chat Look and Feel pt2 & Startup question --- .../android/app/src/main/AndroidManifest.xml | 8 ++ Frontend/ios/Runner/Info.plist | 4 + .../mih_providers/mzansi_ai_provider.dart | 92 ++++++++++++++-- .../mih_providers/ollama_provider.dart | 4 + .../package_tools/mih_personal_home.dart | 10 +- .../lib/mih_packages/mzansi_ai/mzansi_ai.dart | 11 +- .../mzansi_ai/package_tools/ai_chat.dart | 16 ++- .../mzansi_ai/package_tools/mih_ai_chat.dart | 100 +++++++++++------- .../Flutter/GeneratedPluginRegistrant.swift | 2 + Frontend/pubspec.lock | 58 +++++----- Frontend/pubspec.yaml | 3 +- .../flutter/generated_plugin_registrant.cc | 3 + .../windows/flutter/generated_plugins.cmake | 1 + 13 files changed, 225 insertions(+), 87 deletions(-) diff --git a/Frontend/android/app/src/main/AndroidManifest.xml b/Frontend/android/app/src/main/AndroidManifest.xml index 74a51a4f..6bdf2e76 100644 --- a/Frontend/android/app/src/main/AndroidManifest.xml +++ b/Frontend/android/app/src/main/AndroidManifest.xml @@ -17,6 +17,11 @@ + + + + + + + + diff --git a/Frontend/ios/Runner/Info.plist b/Frontend/ios/Runner/Info.plist index 53e112f3..e74bd533 100644 --- a/Frontend/ios/Runner/Info.plist +++ b/Frontend/ios/Runner/Info.plist @@ -2,6 +2,10 @@ + NSMicrophoneUsageDescription + This app needs access to your microphone to enable voice input for the chat. + NSSpeechRecognitionUsageDescription + This app uses speech recognition to convert your voice messages into text. SKAdNetworkItems diff --git a/Frontend/lib/mih_components/mih_providers/mzansi_ai_provider.dart b/Frontend/lib/mih_components/mih_providers/mzansi_ai_provider.dart index a514acfc..7f3e35e8 100644 --- a/Frontend/lib/mih_components/mih_providers/mzansi_ai_provider.dart +++ b/Frontend/lib/mih_components/mih_providers/mzansi_ai_provider.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart'; +import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:mzansi_innovation_hub/main.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_icons.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_providers/ollama_provider.dart'; @@ -63,6 +64,75 @@ class MzansiAiProvider extends ChangeNotifier { notifyListeners(); } + void clearStartUpQuestion() { + startUpQuestion = null; + } + + MarkdownStyleSheet getLlmChatMarkdownStyle(BuildContext context) { + TextStyle body = TextStyle( + color: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + fontSize: 16, + fontWeight: FontWeight.w400, + ); + TextStyle heading1 = TextStyle( + color: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + fontSize: 24, + fontWeight: FontWeight.w400, + ); + TextStyle heading2 = TextStyle( + color: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + fontSize: 20, + fontWeight: FontWeight.w400, + ); + TextStyle code = TextStyle( + color: MihColors.getSecondaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + fontSize: 16, + fontWeight: FontWeight.w400, + ); + BoxDecoration codeBlock = BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10), + bottomLeft: Radius.circular(10), + bottomRight: Radius.circular(10), + ), + color: MihColors.getHighlightColor( + MzansiInnovationHub.of(context)!.theme.mode != "Dark"), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(76), + blurRadius: 8, + offset: Offset(2, 2), + ), + ], + ); + return MarkdownStyleSheet( + a: body, + blockquote: body, + checkbox: body, + del: body, + em: body.copyWith(fontStyle: FontStyle.italic), + h1: heading1, + h2: heading2, + h3: body.copyWith(fontWeight: FontWeight.bold), + h4: body, + h5: body, + h6: body, + listBullet: body, + img: body, + strong: body.copyWith(fontWeight: FontWeight.bold), + p: body, + tableBody: body, + tableHead: body, + code: code, + codeblockDecoration: codeBlock, + ); + } + LlmChatViewStyle? getChatStyle(BuildContext context) { return LlmChatViewStyle( backgroundColor: MihColors.getPrimaryColor( @@ -153,6 +223,7 @@ class MzansiAiProvider extends ChangeNotifier { ), ], ), + markdownStyle: getLlmChatMarkdownStyle(context), ), // User Chat Style userMessageStyle: UserMessageStyle( @@ -231,17 +302,18 @@ class MzansiAiProvider extends ChangeNotifier { MzansiInnovationHub.of(context)!.theme.mode == "Dark"), ), cancelButtonStyle: ActionButtonStyle( - iconDecoration: BoxDecoration( - color: MihColors.getRedColor( - MzansiInnovationHub.of(context)!.theme.mode == "Dark"), - borderRadius: BorderRadius.circular(25), - ), - iconColor: MihColors.getSecondaryInvertedColor( + iconDecoration: BoxDecoration( + color: MihColors.getRedColor( MzansiInnovationHub.of(context)!.theme.mode == "Dark"), - textStyle: TextStyle( - color: MihColors.getPrimaryColor( - MzansiInnovationHub.of(context)!.theme.mode == "Dark"), - )), + borderRadius: BorderRadius.circular(25), + ), + iconColor: MihColors.getSecondaryInvertedColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + textStyle: TextStyle( + color: MihColors.getPrimaryColor( + MzansiInnovationHub.of(context)!.theme.mode == "Dark"), + ), + ), ); } } diff --git a/Frontend/lib/mih_components/mih_providers/ollama_provider.dart b/Frontend/lib/mih_components/mih_providers/ollama_provider.dart index 5b4ee268..e344279d 100644 --- a/Frontend/lib/mih_components/mih_providers/ollama_provider.dart +++ b/Frontend/lib/mih_components/mih_providers/ollama_provider.dart @@ -2,6 +2,7 @@ import 'dart:convert'; 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'; class OllamaProvider extends LlmProvider with ChangeNotifier { @@ -40,16 +41,19 @@ class OllamaProvider extends LlmProvider with ChangeNotifier { String prompt, { Iterable attachments = const [], }) async* { + KenLogger.success("sendMessageStream called with: $prompt"); final userMessage = ChatMessage.user(prompt, attachments); final llmMessage = ChatMessage.llm(); _history.addAll([userMessage, llmMessage]); notifyListeners(); + KenLogger.success("History after adding messages: ${_history.length}"); final messages = _mapToOllamaMessages(_history); final stream = _generateStream(messages); yield* stream.map((chunk) { llmMessage.append(chunk); return chunk; }); + KenLogger.success("Stream completed for: $prompt"); notifyListeners(); } diff --git a/Frontend/lib/mih_packages/mih_home/package_tools/mih_personal_home.dart b/Frontend/lib/mih_packages/mih_home/package_tools/mih_personal_home.dart index 6daf28fb..1c3789ed 100644 --- a/Frontend/lib/mih_packages/mih_home/package_tools/mih_personal_home.dart +++ b/Frontend/lib/mih_packages/mih_home/package_tools/mih_personal_home.dart @@ -286,11 +286,11 @@ class _MihPersonalHomeState extends State hintColor: MihColors.getPrimaryColor( MzansiInnovationHub.of(context)!.theme.mode == "Dark"), onPrefixIconTap: () { - mzansiAiProvider - .setStartUpQuestion(searchController.text); - context.goNamed( - "mzansiAi", - ); + if (searchController.text.isNotEmpty) { + mzansiAiProvider + .setStartUpQuestion(searchController.text); + } + context.goNamed("mzansiAi"); searchController.clear(); }, searchFocusNode: _searchFocusNode, diff --git a/Frontend/lib/mih_packages/mzansi_ai/mzansi_ai.dart b/Frontend/lib/mih_packages/mzansi_ai/mzansi_ai.dart index 87813973..cea2fd78 100644 --- a/Frontend/lib/mih_packages/mzansi_ai/mzansi_ai.dart +++ b/Frontend/lib/mih_packages/mzansi_ai/mzansi_ai.dart @@ -3,7 +3,6 @@ import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_ import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_action.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_package_tools.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_providers/mzansi_ai_provider.dart'; -import 'package:mzansi_innovation_hub/mih_packages/mzansi_ai/package_tools/ai_chat.dart'; import 'package:flutter/material.dart'; import 'package:mzansi_innovation_hub/mih_packages/mzansi_ai/package_tools/mih_ai_chat.dart'; import 'package:provider/provider.dart'; @@ -37,9 +36,9 @@ class _MzansiAiState extends State { temp[const Icon(Icons.chat)] = () { context.read().setToolIndex(0); }; - temp[const Icon(Icons.chat)] = () { - context.read().setToolIndex(1); - }; + // temp[const Icon(Icons.chat)] = () { + // context.read().setToolIndex(1); + // }; return MihPackageTools( tools: temp, @@ -49,7 +48,7 @@ class _MzansiAiState extends State { List getToolBody() { List toolBodies = [ - AiChat(), + // AiChat(), MihAiChat(), ]; return toolBodies; @@ -58,7 +57,7 @@ class _MzansiAiState extends State { List getToolTitle() { List toolTitles = [ "Ask Mzansi", - "New Chat", + // "New Chat", ]; return toolTitles; } diff --git a/Frontend/lib/mih_packages/mzansi_ai/package_tools/ai_chat.dart b/Frontend/lib/mih_packages/mzansi_ai/package_tools/ai_chat.dart index 61c9dbc9..3d6df16e 100644 --- a/Frontend/lib/mih_packages/mzansi_ai/package_tools/ai_chat.dart +++ b/Frontend/lib/mih_packages/mzansi_ai/package_tools/ai_chat.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; -import 'package:gpt_markdown/gpt_markdown.dart'; import 'package:mzansi_innovation_hub/main.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_package_components/mih_icons.dart'; import 'package:mzansi_innovation_hub/mih_components/mih_providers/mzansi_ai_provider.dart'; @@ -221,8 +220,21 @@ class _AiChatState extends State { child: Column( mainAxisSize: MainAxisSize.max, children: [ + // SelectionArea( + // child: GptMarkdown( + // snapshot.requireData, + // textAlign: TextAlign.left, + // style: TextStyle( + // color: MihColors.getSecondaryColor( + // MzansiInnovationHub.of(context)!.theme.mode == + // "Dark"), + // fontSize: _chatFrontSize, + // fontWeight: FontWeight.bold, + // ), + // ), + // ), SelectionArea( - child: GptMarkdown( + child: Text( snapshot.requireData, textAlign: TextAlign.left, style: TextStyle( 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 5865ff8e..849edef8 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 @@ -124,54 +124,68 @@ class _MihAiChatState extends State { } Future initTts() async { - List _voices = []; - List _voicesString = []; - // await _flutterTts.setLanguage("en-US"); - await _flutterTts.setSpeechRate(1); - _flutterTts.getVoices.then( - (data) { - try { - _voices = List.from(data); + try { + await _flutterTts.setSpeechRate(1); + // await _flutterTts.setLanguage("en-US"); - setState(() { - _voices = _voices - .where( - (_voice) => _voice["name"].toLowerCase().contains("en-us")) - .toList(); - _voicesString = - _voices.map((_voice) => _voice["name"] as String).toList(); - _voicesString.sort(); - _flutterTts.setVoice( - { - "name": _voicesString.first, - "locale": _voices - .where((_voice) => - _voice["name"].contains(_voicesString.first)) - .first["locale"] - }, - ); - }); + // Safer voice selection with error handling + _flutterTts.getVoices.then((data) { + try { + final voices = List.from(data); + final englishVoices = voices.where((voice) { + final name = voice["name"]?.toString().toLowerCase() ?? ''; + final locale = voice["locale"]?.toString().toLowerCase() ?? ''; + return name.contains("en-us") || locale.contains("en_us"); + }).toList(); + + if (englishVoices.isNotEmpty) { + // Use the first available English voice + _flutterTts.setVoice({"name": englishVoices.first["name"]}); + } + // If no voices found, use default } catch (e) { - print(e); + KenLogger.error("Error setting TTS voice: $e"); } - }, - ); - _flutterTts.setStartHandler(() { - setState(() { - ttsOn = true; }); + } catch (e) { + KenLogger.error("Error initializing TTS: $e"); + } + + _flutterTts.setStartHandler(() { + if (mounted) { + setState(() { + ttsOn = true; + }); + } }); _flutterTts.setCompletionHandler(() { - setState(() { - ttsOn = false; - }); + if (mounted) { + setState(() { + ttsOn = false; + }); + } }); _flutterTts.setErrorHandler((message) { - setState(() { - ttsOn = false; - }); + if (mounted) { + setState(() { + ttsOn = false; + }); + } + }); + } + + void initStartQuestion() { + WidgetsBinding.instance.addPostFrameCallback((_) async { + final mzansiAiProvider = context.read(); + final startQuestion = mzansiAiProvider.startUpQuestion; + if (startQuestion != null && startQuestion.isNotEmpty) { + final stream = + mzansiAiProvider.ollamaProvider.sendMessageStream(startQuestion); + stream.listen((chunk) {}); + mzansiAiProvider.clearStartUpQuestion(); + } }); } @@ -179,6 +193,7 @@ class _MihAiChatState extends State { void initState() { super.initState(); initTts(); + initStartQuestion(); } @override @@ -192,6 +207,13 @@ class _MihAiChatState extends State { return Consumer( builder: (BuildContext context, MzansiAiProvider mzansiAiProvider, Widget? child) { + // final startupQuestion = mzansiAiProvider.startUpQuestion; + // if (startupQuestion != null) { + // WidgetsBinding.instance.addPostFrameCallback((_) { + // mzansiAiProvider.ollamaProvider.sendMessageStream(startupQuestion); + // mzansiAiProvider.setStartUpQuestion(null); + // }); + // } bool hasHistory = mzansiAiProvider.ollamaProvider.history.isNotEmpty; KenLogger.success("has history: $hasHistory"); KenLogger.success( @@ -200,8 +222,10 @@ class _MihAiChatState extends State { children: [ LlmChatView( provider: mzansiAiProvider.ollamaProvider, + messageSender: mzansiAiProvider.ollamaProvider.sendMessageStream, // welcomeMessage: // "Mzansi AI is here to help. Send us a messahe and we'll try our best to assist you.", + autofocus: false, enableAttachments: false, enableVoiceNotes: true, style: mzansiAiProvider.getChatStyle(context), diff --git a/Frontend/macos/Flutter/GeneratedPluginRegistrant.swift b/Frontend/macos/Flutter/GeneratedPluginRegistrant.swift index 3cef27da..25486c96 100644 --- a/Frontend/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/Frontend/macos/Flutter/GeneratedPluginRegistrant.swift @@ -24,6 +24,7 @@ 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 @@ -49,6 +50,7 @@ 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 e6a2d08a..36b32dae 100644 --- a/Frontend/pubspec.lock +++ b/Frontend/pubspec.lock @@ -703,21 +703,13 @@ packages: source: hosted version: "6.0.0" flutter_markdown_plus: - dependency: transitive + dependency: "direct main" description: name: flutter_markdown_plus sha256: "7f349c075157816da399216a4127096108fd08e1ac931e34e72899281db4113c" url: "https://pub.dev" source: hosted version: "1.0.5" - flutter_math_fork: - dependency: transitive - description: - name: flutter_math_fork - sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407" - url: "https://pub.dev" - source: hosted - version: "0.7.4" flutter_native_splash: dependency: "direct main" description: @@ -904,14 +896,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" - gpt_markdown: - dependency: "direct main" - description: - name: gpt_markdown - sha256: "68d5337c8a00fc03a37dbddf84a6fd90401c30e99b6baf497ef9522a81fc34ee" - url: "https://pub.dev" - source: hosted - version: "1.1.2" graphs: dependency: transitive description: @@ -1360,6 +1344,14 @@ 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: @@ -1741,6 +1733,30 @@ 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: @@ -1933,14 +1949,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - tuple: - dependency: transitive - description: - name: tuple - sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 - url: "https://pub.dev" - source: hosted - version: "2.0.2" typed_data: dependency: transitive description: diff --git a/Frontend/pubspec.yaml b/Frontend/pubspec.yaml index 243368fb..95ec990b 100644 --- a/Frontend/pubspec.yaml +++ b/Frontend/pubspec.yaml @@ -54,12 +54,13 @@ dependencies: go_router: ^16.1.0 screen_brightness: ^2.1.6 cached_network_image: ^3.4.1 - gpt_markdown: ^1.1.2 upgrader: ^12.0.0 screenshot: ^3.0.0 file_saver: ^0.3.1 provider: ^6.1.5+1 flutter_ai_toolkit: ^0.10.0 + flutter_markdown_plus: ^1.0.5 + speech_to_text: ^7.3.0 dev_dependencies: flutter_test: diff --git a/Frontend/windows/flutter/generated_plugin_registrant.cc b/Frontend/windows/flutter/generated_plugin_registrant.cc index ad71ace9..f08708cc 100644 --- a/Frontend/windows/flutter/generated_plugin_registrant.cc +++ b/Frontend/windows/flutter/generated_plugin_registrant.cc @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -46,6 +47,8 @@ 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 57f48306..46e96ea5 100644 --- a/Frontend/windows/flutter/generated_plugins.cmake +++ b/Frontend/windows/flutter/generated_plugins.cmake @@ -15,6 +15,7 @@ list(APPEND FLUTTER_PLUGIN_LIST record_windows screen_brightness_windows share_plus + speech_to_text_windows syncfusion_pdfviewer_windows url_launcher_windows )