diff --git a/Frontend/lib/mih_components/mih_package/mih_app.dart b/Frontend/lib/mih_components/mih_package/mih_app.dart index 7c67609a..dd0270ab 100644 --- a/Frontend/lib/mih_components/mih_package/mih_app.dart +++ b/Frontend/lib/mih_components/mih_package/mih_app.dart @@ -1,4 +1,3 @@ -import 'package:Mzansi_Innovation_Hub/mih_components/mih_package/mih-app_tool_body.dart'; import 'package:Mzansi_Innovation_Hub/mih_components/mih_package/mih_app_action.dart'; import 'package:Mzansi_Innovation_Hub/mih_components/mih_package/mih_app_tools.dart'; import 'package:flutter/material.dart'; @@ -8,7 +7,7 @@ import 'package:flutter_swipe_detector/flutter_swipe_detector.dart'; class MihApp extends StatefulWidget { final MihAppAction appActionButton; final MihAppTools appTools; - final List appBody; + final List appBody; int selectedbodyIndex; final onIndexChange; MihApp({ @@ -34,6 +33,7 @@ class _MihAppState extends State { width: screenSize.width, height: screenSize.height, //color: Colors.black, + padding: const EdgeInsets.only(top: 5), child: Column( children: [ Row( diff --git a/Frontend/lib/mih_env/env.dart b/Frontend/lib/mih_env/env.dart index 6c386f7e..c96e44e0 100644 --- a/Frontend/lib/mih_env/env.dart +++ b/Frontend/lib/mih_env/env.dart @@ -31,6 +31,7 @@ abstract class AppEnviroment { { baseApiUrl = "https://api.mzansi-innovation-hub.co.za"; baseFileUrl = "https://minio.mzansi-innovation-hub.co.za"; + baseAiUrl = "https://ai.mzansi-innovation-hub.co.za"; //fingerPrintPluginKey = 'h5X7a5j14iUZCobI1ZeX'; break; } diff --git a/Frontend/lib/mih_packages/mih_home/mih_home.dart b/Frontend/lib/mih_packages/mih_home/mih_home.dart index 431678f1..8b6e79d1 100644 --- a/Frontend/lib/mih_packages/mih_home/mih_home.dart +++ b/Frontend/lib/mih_packages/mih_home/mih_home.dart @@ -228,6 +228,7 @@ class _MIHHomeState extends State { onTap: () { Navigator.of(context).pushNamed( '/mzansi-ai', + arguments: widget.signedInUser, ); }, tileName: "Mzansi AI", diff --git a/Frontend/lib/mih_packages/mzansi_ai/ai_chat.dart b/Frontend/lib/mih_packages/mzansi_ai/ai_chat.dart new file mode 100644 index 00000000..5426ed60 --- /dev/null +++ b/Frontend/lib/mih_packages/mzansi_ai/ai_chat.dart @@ -0,0 +1,252 @@ +import 'dart:convert'; + +import 'package:Mzansi_Innovation_Hub/main.dart'; +import 'package:Mzansi_Innovation_Hub/mih_components/mih_inputs_and_buttons/mih_dropdown_input.dart'; +import 'package:Mzansi_Innovation_Hub/mih_components/mih_package/mih-app_tool_body.dart'; +import 'package:Mzansi_Innovation_Hub/mih_components/mih_pop_up_messages/mih_loading_circle.dart'; +import 'package:Mzansi_Innovation_Hub/mih_env/env.dart'; +import 'package:Mzansi_Innovation_Hub/mih_objects/app_user.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_chat_ui/flutter_chat_ui.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart' as types; +import 'package:flutter/services.dart' show rootBundle; +import 'package:ollama_dart/ollama_dart.dart' as ollama; +import 'package:uuid/uuid.dart'; + +class AiChat extends StatefulWidget { + final AppUser signedInUser; + const AiChat({ + super.key, + required this.signedInUser, + }); + + @override + State createState() => _AiChatState(); +} + +class _AiChatState extends State { + TextEditingController _modelCopntroller = TextEditingController(); + List _messages = []; + late types.User _user; + late types.User _mihAI; + String systemPromt = + "You are a helpful and friendly AI assistant. You are running on a system called MIH which was created by \"Mzansi Innovation Hub\" a South African based company."; + final client = ollama.OllamaClient(baseUrl: "${AppEnviroment.baseAiUrl}/api"); + List _chatHistory = []; + + void _addMessage(types.Message message) { + setState(() { + _messages.insert(0, message); + }); + } + + void _handleSendPressed(types.PartialText message) { + final textMessage = types.TextMessage( + author: _user, + createdAt: DateTime.now().millisecondsSinceEpoch, + id: const Uuid().v4(), + text: message.text, + ); + //Add user prompt to history + setState(() { + _chatHistory.add( + ollama.Message( + role: ollama.MessageRole.user, + content: message.text, + ), + ); + }); + + _addMessage(textMessage); + + _handleMessageBack(message.text); + } + + void _handleMessageBack(String userMessage) async { + showDialog( + context: context, + builder: (context) { + return const Mihloadingcircle(); + }, + ); + types.TextMessage textMessage; + String aiResponse = ""; + await _generateChatCompletionWithHistory(userMessage, client) + .then((response) { + aiResponse = response.split("").last.trim(); + }); + setState(() { + _chatHistory.add( + ollama.Message( + role: ollama.MessageRole.assistant, + content: aiResponse, + ), + ); + }); + textMessage = types.TextMessage( + author: _mihAI, + createdAt: DateTime.now().millisecondsSinceEpoch, + id: const Uuid().v4(), + text: aiResponse //message.text, + ); + + _addMessage(textMessage); + print(_chatHistory.toString()); + Navigator.of(context).pop(); + } + + void _loadMessages() async { + final response = await rootBundle.loadString('assets/messages.json'); + final messages = (jsonDecode(response) as List) + .map((e) => types.Message.fromJson(e as Map)) + .toList(); + + setState(() { + _messages = messages; + }); + } + + Future _generateChatCompletionWithHistory( + String userMessage, + final ollama.OllamaClient client, + ) async { + final generated = await client.generateChatCompletion( + request: ollama.GenerateChatCompletionRequest( + model: _modelCopntroller.text, + messages: _chatHistory, + ), + ); + return generated.message.content; + } + + void _resetChat() { + setState(() { + _messages = []; + _chatHistory = []; + + _loadMessages(); + }); + // Navigator.of(context).popAndPushNamed( + // '/mzansi-ai', + // arguments: widget.signedInUser, + // ); + } + + ChatTheme getChatTheme() { + return DarkChatTheme( + backgroundColor: MzanziInnovationHub.of(context)!.theme.primaryColor(), + inputBackgroundColor: + MzanziInnovationHub.of(context)!.theme.secondaryColor(), + inputTextColor: MzanziInnovationHub.of(context)!.theme.primaryColor(), + inputTextCursorColor: + MzanziInnovationHub.of(context)!.theme.primaryColor(), + primaryColor: MzanziInnovationHub.of(context)!.theme.secondaryColor(), + secondaryColor: MzanziInnovationHub.of(context)!.theme.successColor(), + errorColor: MzanziInnovationHub.of(context)!.theme.errorColor(), + sentMessageBodyTextStyle: TextStyle( + color: MzanziInnovationHub.of(context)!.theme.primaryColor(), + fontSize: 17, + fontWeight: FontWeight.w500, + fontFamily: 'Segoe UI', + ), + receivedMessageBodyTextStyle: TextStyle( + color: MzanziInnovationHub.of(context)!.theme.primaryColor(), + fontSize: 17, + fontWeight: FontWeight.w500, + fontFamily: 'Segoe UI', + ), + emptyChatPlaceholderTextStyle: TextStyle( + color: MzanziInnovationHub.of(context)!.theme.messageTextColor(), + fontSize: 17, + fontWeight: FontWeight.w500, + fontFamily: 'Segoe UI', + ), + ); + } + + @override + void initState() { + super.initState(); + _user = types.User( + firstName: widget.signedInUser.fname, + id: widget.signedInUser.app_id, //'82091008-a484-4a89-ae75-a22bf8d6f3ac', + ); + _mihAI = types.User( + firstName: "Mzansi AI", + id: const Uuid().v4(), + ); + _modelCopntroller.text = 'deepseek-r1:1.5b'; + // _chatHistory.add( + // ollama.Message( + // role: ollama.MessageRole.system, + // content: systemPromt, + // ), + // ); + _loadMessages(); + } + + @override + Widget build(BuildContext context) { + return MihAppToolBody( + borderOn: false, + bodyItem: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Chat with AI", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + color: + MzanziInnovationHub.of(context)!.theme.secondaryColor(), + ), + ), + IconButton( + onPressed: () { + _resetChat(); + }, + icon: const Icon(Icons.refresh), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: SizedBox( + width: 300, + child: MIHDropdownField( + controller: _modelCopntroller, + hintText: "AI Model", + dropdownOptions: const ['deepseek-r1:1.5b'], + required: true, + editable: true, + ), + ), + ) + ], + ), + Expanded( + child: Chat( + messages: _messages, + // onAttachmentPressed: _handleAttachmentPressed, + // onMessageTap: _handleMessageTap, + // onPreviewDataFetched: _handlePreviewDataFetched, + onSendPressed: _handleSendPressed, + showUserAvatars: false, + showUserNames: false, + user: _user, + theme: getChatTheme(), + ), + ) + ], + ), + ); + } +} diff --git a/Frontend/lib/mih_packages/mzansi_ai/mzansi_ai.dart b/Frontend/lib/mih_packages/mzansi_ai/mzansi_ai.dart new file mode 100644 index 00000000..0115b770 --- /dev/null +++ b/Frontend/lib/mih_packages/mzansi_ai/mzansi_ai.dart @@ -0,0 +1,78 @@ +import 'package:Mzansi_Innovation_Hub/mih_components/mih_package/mih_app.dart'; +import 'package:Mzansi_Innovation_Hub/mih_components/mih_package/mih_app_action.dart'; +import 'package:Mzansi_Innovation_Hub/mih_components/mih_package/mih_app_tools.dart'; +import 'package:Mzansi_Innovation_Hub/mih_objects/app_user.dart'; +import 'package:Mzansi_Innovation_Hub/mih_objects/arguments.dart'; +import 'package:Mzansi_Innovation_Hub/mih_packages/mzansi_ai/ai_chat.dart'; +import 'package:flutter/material.dart'; + +class MzansiAi extends StatefulWidget { + final AppUser signedInUser; + const MzansiAi({ + super.key, + required this.signedInUser, + }); + + @override + State createState() => _MzansiAiState(); +} + +class _MzansiAiState extends State { + int _selcetedIndex = 0; + + MihAppAction getAction() { + return MihAppAction( + icon: const Icon(Icons.arrow_back), + iconSize: 35, + onTap: () { + Navigator.of(context).pop(); + Navigator.of(context).popAndPushNamed( + '/', + arguments: AuthArguments(true, false), + ); + }, + ); + } + + MihAppTools getTools() { + Map temp = {}; + temp[const Icon(Icons.chat)] = () { + setState(() { + _selcetedIndex = 0; + }); + }; + + return MihAppTools( + tools: temp, + selcetedIndex: _selcetedIndex, + ); + } + + List getToolBody() { + List toolBodies = [ + AiChat(signedInUser: widget.signedInUser), + ]; + return toolBodies; + } + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return MihApp( + appActionButton: getAction(), + appTools: getTools(), + appBody: getToolBody(), + selectedbodyIndex: _selcetedIndex, + onIndexChange: (newValue) { + setState(() { + _selcetedIndex = newValue; + }); + print("Index: $_selcetedIndex"); + }, + ); + } +} diff --git a/Frontend/lib/mih_router/routeGenerator.dart b/Frontend/lib/mih_router/routeGenerator.dart index 755d9f18..444d9501 100644 --- a/Frontend/lib/mih_router/routeGenerator.dart +++ b/Frontend/lib/mih_router/routeGenerator.dart @@ -2,6 +2,7 @@ import 'package:Mzansi_Innovation_Hub/mih_components/mih_package/test/package_te import 'package:Mzansi_Innovation_Hub/mih_packages/calculator/calculator.dart'; import 'package:Mzansi_Innovation_Hub/mih_packages/mih_policy_tos/mih_privacy_polocy.dart'; import 'package:Mzansi_Innovation_Hub/mih_packages/mih_policy_tos/mih_terms_of_service.dart'; +import 'package:Mzansi_Innovation_Hub/mih_packages/mzansi_ai/mzansi_ai.dart'; import 'package:Mzansi_Innovation_Hub/mih_packages/mzansi_wallet/mih_barcode_scanner.dart'; import 'package:Mzansi_Innovation_Hub/mih_packages/mzansi_wallet/mzansi_wallet.dart'; import 'package:flutter/material.dart'; @@ -293,14 +294,20 @@ class RouteGenerator { //=============================================================== - //Calculator + //Mzansi AI + case '/mzansi-ai': + if (args is AppUser) { + return MaterialPageRoute( + settings: settings, + builder: (_) => MzansiAi( + signedInUser: args, + ), + ); + } + return _errorRoute(); + //=============================================================== - // case '/mzansi-ai': - // return MaterialPageRoute( - // settings: settings, - // builder: (_) => const MzansiAi(), - // ); - + //Package Ttemplate Test case '/package-dev': return MaterialPageRoute( settings: settings, diff --git a/Frontend/pubspec.lock b/Frontend/pubspec.lock index 75606494..86aa6ea5 100644 --- a/Frontend/pubspec.lock +++ b/Frontend/pubspec.lock @@ -297,6 +297,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + diffutil_dart: + dependency: transitive + description: + name: diffutil_dart + sha256: "5e74883aedf87f3b703cb85e815bdc1ed9208b33501556e4a8a5572af9845c81" + url: "https://pub.dev" + source: hosted + version: "4.0.1" dio: dependency: transitive description: @@ -313,6 +321,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -406,6 +422,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_chat_types: + dependency: "direct main" + description: + name: flutter_chat_types + sha256: e285b588f6d19d907feb1f6d912deaf22e223656769c34093b64e1c59b094fb9 + url: "https://pub.dev" + source: hosted + version: "3.6.2" + flutter_chat_ui: + dependency: "direct main" + description: + name: flutter_chat_ui + sha256: "168a4231464ad00a17ea5f0813f1b58393bdd4035683ea4dc37bbe26be62891e" + url: "https://pub.dev" + source: hosted + version: "1.6.15" flutter_launcher_icons: dependency: "direct main" description: @@ -414,6 +446,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.1" + flutter_link_previewer: + dependency: transitive + description: + name: flutter_link_previewer + sha256: "007069e60f42419fb59872beb7a3cc3ea21e9f1bdff5d40239f376fa62ca9f20" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + flutter_linkify: + dependency: transitive + description: + name: flutter_linkify + sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" + url: "https://pub.dev" + source: hosted + version: "6.0.0" flutter_lints: dependency: "direct dev" description: @@ -430,6 +478,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + flutter_parsed_text: + dependency: transitive + description: + name: flutter_parsed_text + sha256: "529cf5793b7acdf16ee0f97b158d0d4ba0bf06e7121ef180abe1a5b59e32c1e2" + url: "https://pub.dev" + source: hosted + version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -656,6 +712,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + linkify: + dependency: transitive + description: + name: linkify + sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" + url: "https://pub.dev" + source: hosted + version: "5.0.0" lints: dependency: transitive description: @@ -912,6 +976,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + photo_view: + dependency: transitive + description: + name: photo_view + sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" + url: "https://pub.dev" + source: hosted + version: "0.15.0" platform: dependency: transitive description: @@ -984,6 +1056,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + scroll_to_index: + dependency: transitive + description: + name: scroll_to_index + sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 + url: "https://pub.dev" + source: hosted + version: "3.0.1" shared_preferences: dependency: transitive description: @@ -1342,7 +1422,7 @@ packages: source: hosted version: "3.1.3" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff @@ -1357,6 +1437,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" vm_service: dependency: transitive description: diff --git a/Frontend/pubspec.yaml b/Frontend/pubspec.yaml index 71f0d8ea..d7893012 100644 --- a/Frontend/pubspec.yaml +++ b/Frontend/pubspec.yaml @@ -70,6 +70,9 @@ dependencies: local_auth: ^2.3.0 math_expressions: ^2.6.0 ollama_dart: ^0.2.2+1 + flutter_chat_ui: ^1.6.15 + flutter_chat_types: ^3.6.2 + uuid: ^4.5.1 dev_dependencies: flutter_test: diff --git a/docker-compose.yml b/docker-compose.yml index eaa59dd2..059e3168 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -114,7 +114,7 @@ services: - certbotConf:/etc/letsencrypt - certbotChall:/var/www/certbot #command: certonly --test-cert --webroot -w /var/www/certbot --force-renewal --email yasienmeth@gmail.com -d mzansi-innovation-hub.co.za -d www.mzansi-innovation-hub.co.za --agree-tos - command: certonly --webroot -w /var/www/certbot --force-renewal --email yasienmeth@gmail.com -d app.mzansi-innovation-hub.co.za -d api.mzansi-innovation-hub.co.za -d minio.mzansi-innovation-hub.co.za -d monitor.mzansi-innovation-hub.co.za --agree-tos + command: certonly --webroot -w /var/www/certbot --force-renewal --email yasienmeth@gmail.com -d app.mzansi-innovation-hub.co.za -d api.mzansi-innovation-hub.co.za -d minio.mzansi-innovation-hub.co.za -d monitor.mzansi-innovation-hub.co.za -d ai.mzansi-innovation-hub.co.za --agree-tos networks: - MIH-network depends_on: diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 03eec203..f2ab1a52 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -116,6 +116,54 @@ http { } } + #================AI Server================ + server { + listen 80; + + server_name ai.mzansi-innovation-hub.co.za; + + # # #Web App + # location / { + # proxy_pass http://MIH-API-Hub:8080/; + # } + + # location ~ /.well-known/acme-challenge/ { + # root /var/www/certbot; + # } + + return 301 https://ai.mzansi-innovation-hub.co.za$request_uri; + } + + server { + listen 443 ssl; + http2 on; + # use the certificates + ssl_certificate /etc/letsencrypt/live/app.mzansi-innovation-hub.co.za/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/app.mzansi-innovation-hub.co.za/privkey.pem; + server_name ai.mzansi-innovation-hub.co.za; + root /var/www/html; + index index.php index.html index.htm; + + # To allow special characters in headers + ignore_invalid_headers off; + # Allow any size file to be uploaded. + # Set to a value such as 1000m; to restrict file size to a specific value + client_max_body_size 0; + # To disable buffering + proxy_buffering off; + proxy_request_buffering off; + + # Web Api + location / { + proxy_pass http://MIH-AI:11434/; + } + + location ~ /.well-known/acme-challenge/ { + root /var/www/certbot; + } + } + + #================Monitor Server================ server { listen 80;