From 234dce65e58b139a6d8a7951696922eb3dc8deff Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Tue, 30 Dec 2025 16:43:56 +0300 Subject: [PATCH] chat bot & Speech to Text --- android/app/src/main/AndroidManifest.xml | 11 + assets/icons/chatbot.svg | 46 +++ assets/icons/microphone.svg | 12 + assets/icons/send.svg | 3 + ios/Runner/Info.plist | 4 +- lib/api/dashboard_api_client.dart | 28 ++ lib/classes/colors.dart | 1 + lib/classes/consts.dart | 4 +- lib/config/routes.dart | 10 +- lib/models/generic_response_model.dart | 153 +++++++- lib/ui/chatbot/chatbot_screen.dart | 334 +++++++++++++++++ lib/ui/landing/dashboard_screen.dart | 456 ++++++++++++----------- pubspec.yaml | 2 + 13 files changed, 830 insertions(+), 234 deletions(-) create mode 100644 assets/icons/chatbot.svg create mode 100644 assets/icons/microphone.svg create mode 100644 assets/icons/send.svg create mode 100644 lib/ui/chatbot/chatbot_screen.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6d2f83f..1cf76f6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,6 +30,17 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/microphone.svg b/assets/icons/microphone.svg new file mode 100644 index 0000000..0cbd4aa --- /dev/null +++ b/assets/icons/microphone.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/send.svg b/assets/icons/send.svg new file mode 100644 index 0000000..37f97e2 --- /dev/null +++ b/assets/icons/send.svg @@ -0,0 +1,3 @@ + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 01197de..5288a28 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -56,7 +56,9 @@ NSLocationWhenInUseUsageDescription This App requires access to your location to mark your attendance. NSMicrophoneUsageDescription - This app requires microphone access to for call. + This app requires microphone access for calls and speech-to-text input. + NSSpeechRecognitionUsageDescription + This app requires speech recognition to convert your voice to text. NSPhotoLibraryUsageDescription This app requires photo library access to select image as document & upload it. UIApplicationSupportsIndirectInputEvents diff --git a/lib/api/dashboard_api_client.dart b/lib/api/dashboard_api_client.dart index 2bddb42..fa4d00a 100644 --- a/lib/api/dashboard_api_client.dart +++ b/lib/api/dashboard_api_client.dart @@ -414,4 +414,32 @@ class DashboardApiClient { // postParams, // ); // } + + Future getChatBotSession() async { + String url = "${ApiConsts.erpRest}Atlas_InitiateChat"; + Map postParams = {}; + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel responseData = GenericResponseModel.fromJson(json); + return responseData; + }, + url, + postParams, + ); + } + + Future sendChatBotMessage({required AtlasInitiateChatResponse atlasChatTokenResponse, required String atlasChatText}) async { + String url = "${ApiConsts.erpRest}Atlas_ContinueChat"; + Map postParams = {"Atlas_AccessToken": atlasChatTokenResponse.atlasAccessToken, "Atlas_ChatSessionID": atlasChatTokenResponse.atlasChatSessionId, "Atlas_ChatText": atlasChatText}; + postParams.addAll(AppState().postParamsJson); + return await ApiClient().postJsonForObject( + (json) { + GenericResponseModel responseData = GenericResponseModel.fromJson(json); + return responseData; + }, + url, + postParams, + ); + } } diff --git a/lib/classes/colors.dart b/lib/classes/colors.dart index 1b0dd06..4c88da9 100644 --- a/lib/classes/colors.dart +++ b/lib/classes/colors.dart @@ -67,4 +67,5 @@ class MyColors { static const Color lightGreyIconColor = Color(0xff919191); static const Color selectedBorderColor = Color(0xff37A4BE); static const Color mazayaRedColor = Color(0xffED1C2B); + static const Color hintTextColor = Color(0xff9AA4B2); } diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index dbec87d..29d22d7 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -9,9 +9,9 @@ class ApiConsts { // static String baseUrl = "https://webservices.hmg.com"; // PreProd // static String baseUrl = "https://hmgwebservices.com"; // Live server - static String baseUrl = "https://mohemm.hmg.com"; // New Live server + // static String baseUrl = "https://mohemm.hmg.com"; // New Live server // - // static String baseUrl = "https://uat.hmgwebservices.com"; // UAT ser343622ver + static String baseUrl = "https://uat.hmgwebservices.com"; // UAT ser343622ver // static String baseUrl = "http://10.20.200.111:1010/"; // static String baseUrl = "https://webservices.hmg.com"; // PreProd diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 4653880..49913b5 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -10,6 +10,7 @@ import 'package:mohem_flutter_app/ui/chat/favorite_users_screen.dart'; import 'package:mohem_flutter_app/ui/chat/group_chat_detaied_screen.dart'; import 'package:mohem_flutter_app/ui/chat/group_members.dart'; import 'package:mohem_flutter_app/ui/chat/manage_group.dart'; +import 'package:mohem_flutter_app/ui/chatbot/chatbot_screen.dart'; import 'package:mohem_flutter_app/ui/landing/dashboard_screen.dart'; import 'package:mohem_flutter_app/ui/landing/itg/change_itg_ad_password_screen.dart'; import 'package:mohem_flutter_app/ui/landing/itg/its_add_screen_video_image.dart'; @@ -32,6 +33,7 @@ import 'package:mohem_flutter_app/ui/misc/request_submit_screen.dart'; import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_input_screen.dart'; import 'package:mohem_flutter_app/ui/my_attendance/dynamic_screens/dynamic_listview_screen.dart'; import 'package:mohem_flutter_app/ui/my_attendance/services_menu_list_screen.dart'; + // import 'package:mohem_flutter_app/ui/my_attendance/my_attendance_screen.dart'; import 'package:mohem_flutter_app/ui/my_team/create_request.dart'; import 'package:mohem_flutter_app/ui/my_team/employee_details.dart'; @@ -54,6 +56,7 @@ import 'package:mohem_flutter_app/ui/profile/profile_screen.dart'; import 'package:mohem_flutter_app/ui/screens/announcements/announcement_details.dart'; import 'package:mohem_flutter_app/ui/screens/announcements/announcements.dart'; import 'package:mohem_flutter_app/ui/screens/child_education/child_education_assistance.dart'; + // import 'package:mohem_flutter_app/ui/my_attendance/work_from_home_screen.dart'; import 'package:mohem_flutter_app/ui/screens/eit/add_eit.dart'; import 'package:mohem_flutter_app/ui/screens/event_activity/event_activity.dart'; @@ -191,6 +194,7 @@ class AppRoutes { static const String chat = "/chat"; static const String chatDetailed = "/chatDetailed"; static const String chatFavoriteUsers = "/chatFavoriteUsers"; + static const String chatBotHome = "/chatBotHome"; //Group Chat static const String manageGroup = "/manageGroup"; @@ -223,8 +227,8 @@ class AppRoutes { newPassword: (BuildContext context) => NewPasswordScreen(), forgotPassword: (BuildContext context) => ForgotPasswordScreen(), todayAttendance: (BuildContext context) => TodayAttendanceScreen2(), - //eit + //eit addEitScreen: (BuildContext context) => AddEITScreen(), //Work List @@ -235,7 +239,6 @@ class AppRoutes { worklistSettings: (BuildContext context) => WorklistSettings(), // Leave Balance - leaveBalance: (BuildContext context) => LeaveBalance(), addLeaveBalance: (BuildContext context) => AddLeaveBalanceScreen(), @@ -312,6 +315,7 @@ class AppRoutes { chat: (BuildContext context) => ChatHome(), chatDetailed: (BuildContext context) => ChatDetailScreen(), chatFavoriteUsers: (BuildContext context) => ChatFavoriteUsersScreen(), + chatBotHome: (BuildContext context) => ChatBotScreen(), //Group Chat manageGroup: (BuildContext context) => ManageGroupScreen(), @@ -328,6 +332,6 @@ class AppRoutes { unsafeDeviceScreen: (BuildContext context) => const UnsafeDeviceScreen(), appUpdateScreen: (BuildContext context) => const AppUpdateScreen(), childEducation: (BuildContext context) => ChildEducationAssistance(), - activityScreen:(BuildContext context) => const EventActivityScreen() + activityScreen: (BuildContext context) => const EventActivityScreen(), }; } diff --git a/lib/models/generic_response_model.dart b/lib/models/generic_response_model.dart index df94c8b..337572c 100644 --- a/lib/models/generic_response_model.dart +++ b/lib/models/generic_response_model.dart @@ -398,6 +398,9 @@ class GenericResponseModel { bool? isActiveCode; bool? isSMSSent; PortalDirectionData? portalDirectionData; + AtlasInitiateChatResponse? atlasInitiateChatResponse; + AtlasContinueChatResponseDetails? atlasContinueChatResponseDetails; + GenericResponseModel({ this.date, @@ -669,7 +672,10 @@ class GenericResponseModel { this.ePharmacyGetItemOnHandList, this.isActiveCode, this.isSMSSent, - this.getFADisposalNtfDetails + this.getFADisposalNtfDetails, + this.atlasInitiateChatResponse, + this.atlasContinueChatResponseDetails, + }); GenericResponseModel.fromJson(Map json) { @@ -943,7 +949,6 @@ class GenericResponseModel { getFADisposalNtfDetails = json['GetFADisposalNtfDetails'] != null ? GetFaDisposalNtfDetails.fromJson(json['GetFADisposalNtfDetails']) : null; - if (json['GetEarningsList'] != null) { getEarningsList = []; json['GetEarningsList'].forEach((v) { @@ -1464,6 +1469,10 @@ class GenericResponseModel { ePharmacyGetItemOnHandList = json['ePharmacy_GetItemOnHandList']; isActiveCode = json['isActiveCode']; isSMSSent = json['isSMSSent']; + atlasInitiateChatResponse = json["Atlas_InitiateChat_Response"] == null ? null : AtlasInitiateChatResponse.fromJson(json["Atlas_InitiateChat_Response"]); + atlasContinueChatResponseDetails = json["Atlas_ContinueChat_ResponseDetails"] == null ? null : AtlasContinueChatResponseDetails.fromJson(json["Atlas_ContinueChat_ResponseDetails"]); + + } Map toJson() { @@ -1561,7 +1570,7 @@ class GenericResponseModel { if (this.getActionHistoryList != null) { data['GetActionHistoryList'] = this.getActionHistoryList!.map((v) => v.toJson()).toList(); } - data['GetFADisposalNtfDetails'] =this.getFADisposalNtfDetails; + data['GetFADisposalNtfDetails'] = this.getFADisposalNtfDetails; data['GetAddressDffStructureList'] = this.getAddressDffStructureList; data['GetAddressNotificationBodyList'] = this.getAddressNotificationBodyList; @@ -1949,6 +1958,9 @@ class GenericResponseModel { data['ePharmacy_GetItemOnHandList'] = this.ePharmacyGetItemOnHandList; data['isActiveCode'] = this.isActiveCode; data['isSMSSent'] = this.isSMSSent; + data["Atlas_InitiateChat_Response"] = atlasInitiateChatResponse?.toJson(); + data["Atlas_ContinueChat_ResponseDetails"] = atlasContinueChatResponseDetails?.toJson(); + return data; } } @@ -1974,3 +1986,138 @@ class TicketBookingResult { TicketBookingResult(this.success, this.clientId); } + +class AtlasInitiateChatResponse { + String? atlasAccessToken; + String? atlasChatSessionId; + String? atlasTokenType; + + AtlasInitiateChatResponse({this.atlasAccessToken, this.atlasChatSessionId, this.atlasTokenType}); + + factory AtlasInitiateChatResponse.fromRawJson(String str) => AtlasInitiateChatResponse.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory AtlasInitiateChatResponse.fromJson(Map json) => + AtlasInitiateChatResponse(atlasAccessToken: json["Atlas_AccessToken"], atlasChatSessionId: json["Atlas_ChatSessionID"], atlasTokenType: json["Atlas_TokenType"]); + + Map toJson() => {"Atlas_AccessToken": atlasAccessToken, "Atlas_ChatSessionID": atlasChatSessionId, "Atlas_TokenType": atlasTokenType}; +} + + + +class AtlasContinueChatResponseDetails { + AiResponse? aiResponse; + QueryClassification? queryClassification; + String? sessionId; + AiResponse? userMessage; + + AtlasContinueChatResponseDetails({ + this.aiResponse, + this.queryClassification, + this.sessionId, + this.userMessage, + }); + + factory AtlasContinueChatResponseDetails.fromRawJson(String str) => AtlasContinueChatResponseDetails.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory AtlasContinueChatResponseDetails.fromJson(Map json) => AtlasContinueChatResponseDetails( + aiResponse: json["ai_response"] == null ? null : AiResponse.fromJson(json["ai_response"]), + queryClassification: json["query_classification"] == null ? null : QueryClassification.fromJson(json["query_classification"]), + sessionId: json["session_id"], + userMessage: json["user_message"] == null ? null : AiResponse.fromJson(json["user_message"]), + ); + + Map toJson() => { + "ai_response": aiResponse?.toJson(), + "query_classification": queryClassification?.toJson(), + "session_id": sessionId, + "user_message": userMessage?.toJson(), + }; +} + +class AiResponse { + DateTime? createdAt; + String? id; + StructuredData? structuredData; + String? text; + + AiResponse({ + this.createdAt, + this.id, + this.structuredData, + this.text, + }); + + factory AiResponse.fromRawJson(String str) => AiResponse.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory AiResponse.fromJson(Map json) => AiResponse( + createdAt: json["created_at"] == null ? null : DateTime.parse(json["created_at"]), + id: json["id"], + structuredData: json["structured_data"] == null ? null : StructuredData.fromJson(json["structured_data"]), + text: json["text"], + ); + + Map toJson() => { + "created_at": createdAt?.toIso8601String(), + "id": id, + "structured_data": structuredData?.toJson(), + "text": text, + }; +} + +class StructuredData { + String? answer; + List? sources; + String? type; + + StructuredData({ + this.answer, + this.sources, + this.type, + }); + + factory StructuredData.fromRawJson(String str) => StructuredData.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory StructuredData.fromJson(Map json) => StructuredData( + answer: json["answer"], + sources: json["sources"] == null ? [] : List.from(json["sources"]!.map((x) => x)), + type: json["type"], + ); + + Map toJson() => { + "answer": answer, + "sources": sources == null ? [] : List.from(sources!.map((x) => x)), + "type": type, + }; +} + +class QueryClassification { + int? confidence; + String? type; + + QueryClassification({ + this.confidence, + this.type, + }); + + factory QueryClassification.fromRawJson(String str) => QueryClassification.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory QueryClassification.fromJson(Map json) => QueryClassification( + confidence: json["confidence"], + type: json["type"], + ); + + Map toJson() => { + "confidence": confidence, + "type": type, + }; +} \ No newline at end of file diff --git a/lib/ui/chatbot/chatbot_screen.dart b/lib/ui/chatbot/chatbot_screen.dart new file mode 100644 index 0000000..5b5989d --- /dev/null +++ b/lib/ui/chatbot/chatbot_screen.dart @@ -0,0 +1,334 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:mohem_flutter_app/api/dashboard_api_client.dart'; +import 'package:mohem_flutter_app/classes/colors.dart'; +import 'package:mohem_flutter_app/classes/utils.dart'; +import 'package:mohem_flutter_app/extensions/widget_extensions.dart'; +import 'package:mohem_flutter_app/models/generic_response_model.dart'; +import 'package:speech_to_text/speech_recognition_result.dart'; +import 'package:speech_to_text/speech_to_text.dart'; + +enum ChatMode { home, chatActive } + +class ChatMessage { + final String text; + final bool isUser; + final DateTime timestamp; + + ChatMessage({required this.text, required this.isUser, required this.timestamp}); +} + +class ChatBotScreen extends StatefulWidget { + const ChatBotScreen({Key? key}) : super(key: key); + + @override + State createState() => _ChatBotScreenState(); +} + +class _ChatBotScreenState extends State { + ChatMode _currentMode = ChatMode.home; + final List _messages = []; + final TextEditingController _messageController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + AtlasInitiateChatResponse? atlasChatTokenResponse; + + // Speech to text + final SpeechToText _speechToText = SpeechToText(); + bool _speechEnabled = false; + + @override + void initState() { + // TODO: implement initState + super.initState(); + print("Chat Bot Launched"); + getChatSession(); + _initSpeech(); + } + + /// Initialize speech to text + void _initSpeech() async { + _speechEnabled = await _speechToText.initialize(); + setState(() {}); + } + + /// Start listening for speech + void _startListening() async { + await _speechToText.listen(onResult: _onSpeechResult); + setState(() {}); + } + + /// Stop listening for speech + void _stopListening() async { + await _speechToText.stop(); + setState(() {}); + } + + /// Callback when speech is recognized + void _onSpeechResult(SpeechRecognitionResult result) { + setState(() { + _messageController.text = result.recognizedWords; + // Move cursor to end of text + _messageController.selection = TextSelection.fromPosition(TextPosition(offset: _messageController.text.length)); + }); + } + + void getChatSession() async { + try { + GenericResponseModel? res = await DashboardApiClient().getChatBotSession(); + if (res != null) { + atlasChatTokenResponse = res.atlasInitiateChatResponse; + } + } catch (ex) { + Utils.handleException(ex, context, null); + } + } + + @override + void dispose() { + atlasChatTokenResponse = null; + + _messageController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + void _sendMessage() async { + String text = _messageController.text.trim(); + if (text.isEmpty || atlasChatTokenResponse == null) return; + + // setState(() { + // _messages.add(ChatMessage(text: text, isUser: true, timestamp: DateTime.now())); + // _currentMode = ChatMode.chatActive; + // _messageController.clear(); + // }); + // + // // Simulate bot response + // Future.delayed(const Duration(milliseconds: 500), () { + // if (mounted) { + // setState(() { + // _messages.add(ChatMessage(text: "Hello there! How may I assist you today?", isUser: false, timestamp: DateTime.now())); + // }); + // _scrollToBottom(); + // } + // }); + + try { + setState(() { + _messages.add(ChatMessage(text: text, isUser: true, timestamp: DateTime.now())); + _currentMode = ChatMode.chatActive; + _messageController.clear(); + }); + GenericResponseModel? res = await DashboardApiClient().sendChatBotMessage(atlasChatTokenResponse: atlasChatTokenResponse!, atlasChatText: text); + if (res != null) { + res.atlasContinueChatResponseDetails; + if (mounted) { + setState(() { + _messages.add( + ChatMessage( + text: res.atlasContinueChatResponseDetails!.aiResponse!.structuredData!.answer ?? "", + isUser: false, + timestamp: res.atlasContinueChatResponseDetails!.aiResponse!.createdAt ?? DateTime.now(), + ), + ); + }); + } + _scrollToBottom(); + } + } catch (ex) { + Utils.handleException(ex, context, null); + } + + _scrollToBottom(); + } + + void _scrollToBottom() { + Future.delayed(const Duration(milliseconds: 100), () { + if (_scrollController.hasClients) { + _scrollController.animateTo(_scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut); + } + }); + } + + void _sendSuggestion(String text) { + _messageController.text = text; + _sendMessage(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: MyColors.backgroundColor, + appBar: AppBar( + title: const Text("ChatBot", style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2B353E))), + backgroundColor: MyColors.backgroundColor, + centerTitle: false, + ), + body: Column(children: [Expanded(child: _currentMode == ChatMode.home ? _buildHomeMode() : _buildChatMode()), _buildBottomInputBar()]), + ); + } + + Widget _buildHomeMode() { + return Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("Hello, ask me", textAlign: TextAlign.center, style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2E303A))), + const Text("anything...", textAlign: TextAlign.center, style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF2E303A))), + const SizedBox(height: 50), + Wrap( + alignment: WrapAlignment.center, + spacing: 8, + runSpacing: 12, + children: [ + _buildSuggestionChip("Nostalgia Perfume", () => _sendSuggestion("Nostalgia Perfume")), + _buildSuggestionChip("Al Nafoura", () => _sendSuggestion("Al Nafoura")), + _buildSuggestionChip("Al Nafoura", () => _sendSuggestion("Al Nafoura")), + _buildSuggestionChip("Al Jadi", () => _sendSuggestion("Al Jadi")), + ], + ), + ], + ), + ), + ); + } + + Widget _buildChatMode() { + return ListView.separated( + controller: _scrollController, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + itemCount: _messages.length, + itemBuilder: (BuildContext context, int index) { + ChatMessage message = _messages[index]; + return _buildMessageBubble(message); + }, + separatorBuilder: (BuildContext context, int index) { + return const SizedBox(height: 15); + }, + ); + } + + Widget _buildMessageBubble(ChatMessage message) { + bool isUser = message.isUser; + return Row( + mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isUser) const Spacer(flex: 2), + Flexible( + flex: 7, + child: Column( + crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: isUser ? Colors.white : null, + gradient: isUser ? null : const LinearGradient(colors: [Color(0xFF32D892), Color(0xFF259CB8)], begin: Alignment.centerLeft, end: Alignment.centerRight), + borderRadius: BorderRadius.circular(15), + boxShadow: isUser ? null : [BoxShadow(color: const Color(0xFF000000).withValues(alpha: 0.0784), offset: const Offset(0, 0), blurRadius: 13, spreadRadius: 0)], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(message.text, softWrap: true, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: isUser ? const Color(0xFF2B353E) : MyColors.white)), + Align( + alignment: Alignment.centerRight, + child: Text( + _formatTimestamp(message.timestamp), + style: TextStyle(fontSize: 11, fontWeight: FontWeight.w500, letterSpacing: -0.4, color: isUser ? const Color(0xFF2B353E) : MyColors.whiteColor), + ), + ), + ], + ), + ), + ], + ), + ), + if (!isUser) const Spacer(flex: 2), + ], + ); + } + + String _formatTimestamp(DateTime timestamp) { + String hour = timestamp.hour.toString().padLeft(2, '0'); + String minute = timestamp.minute.toString().padLeft(2, '0'); + String month = timestamp.month.toString().padLeft(2, '0'); + String day = timestamp.day.toString().padLeft(2, '0'); + int year = timestamp.year; + return "$month/$day/$year $hour:$minute AM"; + } + + Widget _buildSuggestionChip(String label, VoidCallback onTap) { + return Material( + color: Colors.white, + elevation: 3, + shadowColor: const Color(0xFF000015).withValues(alpha: 0.0784), + borderRadius: BorderRadius.circular(10), + child: InkWell( + borderRadius: BorderRadius.circular(10), + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: MyColors.darkTextColor, letterSpacing: 0.2)), + ), + ), + ); + } + + Widget _buildBottomInputBar() { + return Container( + decoration: const BoxDecoration(color: MyColors.backgroundColor), + child: SafeArea( + top: false, + child: Row( + children: [ + // Plus button + // IconButton(padding: EdgeInsets.zero, icon: const Icon(Icons.add, color: MyColors.darkTextColor, size: 34), onPressed: () {}), + // const SizedBox(width: 12), + // Text field + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + height: 48, + decoration: BoxDecoration(color: Colors.transparent, borderRadius: BorderRadius.circular(10), border: BoxBorder.all(width: 1, color: const Color(0xFFE5E5E5))), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _messageController, + onSubmitted: (_) => _sendMessage(), + maxLines: 1, + textAlignVertical: TextAlignVertical.center, + decoration: const InputDecoration( + hintText: 'Type a message..', + hintStyle: TextStyle(color: MyColors.hintTextColor, fontSize: 14, fontWeight: FontWeight.normal), + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 10), + ), + ), + ), + GestureDetector( + onTap: _speechEnabled ? (_speechToText.isNotListening ? _startListening : _stopListening) : null, + child: SvgPicture.asset("assets/icons/microphone.svg", colorFilter: ColorFilter.mode(_speechToText.isListening ? Colors.red : MyColors.darkTextColor, BlendMode.srcIn)), + ), + ], + ), + ), + ), + const SizedBox(width: 6), + // Send button + GestureDetector( + onTap: _sendMessage, + child: Container(width: 48, height: 48, decoration: const BoxDecoration(color: MyColors.white, shape: BoxShape.circle), child: Center(child: SvgPicture.asset("assets/icons/send.svg"))), + ), + ], + ), + ), + ).paddingOnly(left: 16, right: 16, bottom: 16); + } +} diff --git a/lib/ui/landing/dashboard_screen.dart b/lib/ui/landing/dashboard_screen.dart index 3122d17..e87bafe 100644 --- a/lib/ui/landing/dashboard_screen.dart +++ b/lib/ui/landing/dashboard_screen.dart @@ -315,103 +315,103 @@ class _DashboardScreenState extends State with WidgetsBindingOb child: Consumer( builder: (BuildContext context, DashboardProviderModel model, Widget? child) { return (model.isAttendanceTrackingLoading - ? GetAttendanceTrackingShimmer() - : Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - gradient: const LinearGradient( - transform: GradientRotation(.46), - begin: Alignment.topRight, - end: Alignment.bottomLeft, - colors: [MyColors.gradiantEndColor, MyColors.gradiantStartColor], - ), - ), - child: Stack( - alignment: Alignment.center, - children: [ - if (model.isTimeRemainingInSeconds == 0) SvgPicture.asset("assets/images/thumb.svg"), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.markAttendance.tr().toText14(color: Colors.white, isBold: true), - if (model.isTimeRemainingInSeconds == 0) DateTime.now().toString().split(" ")[0].toText12(color: Colors.white), - if (model.isTimeRemainingInSeconds != 0) - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + ? GetAttendanceTrackingShimmer() + : Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + gradient: const LinearGradient( + transform: GradientRotation(.46), + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [MyColors.gradiantEndColor, MyColors.gradiantStartColor], + ), + ), + child: Stack( + alignment: Alignment.center, + children: [ + if (model.isTimeRemainingInSeconds == 0) SvgPicture.asset("assets/images/thumb.svg"), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.markAttendance.tr().toText14(color: Colors.white, isBold: true), + if (model.isTimeRemainingInSeconds == 0) DateTime.now().toString().split(" ")[0].toText12(color: Colors.white), + if (model.isTimeRemainingInSeconds != 0) + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 9.height, + Directionality( + textDirection: ui.TextDirection.ltr, + child: CountdownTimer( + endTime: model.endTime, + onEnd: null, + endWidget: "00:00:00".toText14(color: Colors.white, isBold: true), + textStyle: const TextStyle(color: Colors.white, fontSize: 14, letterSpacing: -0.48, fontWeight: FontWeight.bold), + ), + ), + LocaleKeys.timeLeftToday.tr().toText12(color: Colors.white), + 9.height, + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(20)), + child: LinearProgressIndicator( + value: model.progress, + minHeight: 8, + valueColor: const AlwaysStoppedAnimation(Colors.white), + backgroundColor: const Color(0xff196D73), + ), + ), + ], + ), + ], + ).paddingOnly(top: 12, right: 15, left: 12), + ), + Row( children: [ - 9.height, - Directionality( - textDirection: ui.TextDirection.ltr, - child: CountdownTimer( - endTime: model.endTime, - onEnd: null, - endWidget: "00:00:00".toText14(color: Colors.white, isBold: true), - textStyle: const TextStyle(color: Colors.white, fontSize: 14, letterSpacing: -0.48, fontWeight: FontWeight.bold), - ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LocaleKeys.checkIn.tr().toText12(color: Colors.white), + (model.attendanceTracking!.pSwipeIn == null ? "--:--" : model.attendanceTracking!.pSwipeIn).toString().toText14( + color: Colors.white, + isBold: true, + ), + 4.height, + ], + ).paddingOnly(left: 12, right: 12), ), - LocaleKeys.timeLeftToday.tr().toText12(color: Colors.white), - 9.height, - ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(20)), - child: LinearProgressIndicator( - value: model.progress, - minHeight: 8, - valueColor: const AlwaysStoppedAnimation(Colors.white), - backgroundColor: const Color(0xff196D73), + Container( + margin: EdgeInsets.only(top: AppState().isArabic(context) ? 6 : 0), + width: 45, + height: 45, + padding: const EdgeInsets.only(left: 10, right: 10), + decoration: BoxDecoration( + color: const Color(0xff259EA4), + borderRadius: BorderRadius.only( + bottomRight: AppState().isArabic(context) ? const Radius.circular(0) : const Radius.circular(15), + bottomLeft: AppState().isArabic(context) ? const Radius.circular(15) : const Radius.circular(0), + ), ), - ), + child: SvgPicture.asset(model.isTimeRemainingInSeconds == 0 ? "assets/images/biometrics.svg" : "assets/images/biometrics.svg"), + ).onPress(() { + showMyBottomSheet(context, callBackFunc: () {}, child: MarkAttendanceWidget(model, isFromDashboard: true)); + }), ], ), - ], - ).paddingOnly(top: 12, right: 15, left: 12), - ), - Row( - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - LocaleKeys.checkIn.tr().toText12(color: Colors.white), - (model.attendanceTracking!.pSwipeIn == null ? "--:--" : model.attendanceTracking!.pSwipeIn).toString().toText14( - color: Colors.white, - isBold: true, - ), - 4.height, - ], - ).paddingOnly(left: 12, right: 12), + ], ), - Container( - margin: EdgeInsets.only(top: AppState().isArabic(context) ? 6 : 0), - width: 45, - height: 45, - padding: const EdgeInsets.only(left: 10, right: 10), - decoration: BoxDecoration( - color: const Color(0xff259EA4), - borderRadius: BorderRadius.only( - bottomRight: AppState().isArabic(context) ? const Radius.circular(0) : const Radius.circular(15), - bottomLeft: AppState().isArabic(context) ? const Radius.circular(15) : const Radius.circular(0), - ), - ), - child: SvgPicture.asset(model.isTimeRemainingInSeconds == 0 ? "assets/images/biometrics.svg" : "assets/images/biometrics.svg"), - ).onPress(() { - showMyBottomSheet(context, callBackFunc: () {}, child: MarkAttendanceWidget(model, isFromDashboard: true)); - }), ], ), - ], - ), - ], - ), - ).onPress(() { - Navigator.pushNamed(context, AppRoutes.todayAttendance); - })) + ).onPress(() { + Navigator.pushNamed(context, AppRoutes.todayAttendance); + })) .animatedSwither(); }, ), @@ -460,70 +460,70 @@ class _DashboardScreenState extends State with WidgetsBindingOb flex: 2, child: RichText( text: - AppState().isArabic(context) - ? TextSpan( - children: [ - TextSpan( - text: 'اطلع على مميزات', - style: TextStyle( - fontSize: 16, - letterSpacing: -0.2, - fontFamily: AppState().isArabic(context) ? 'Cairo' : 'Poppins', - fontWeight: FontWeight.w700, - height: 24 / 16, - color: Color(0xFF5D5E5E), - ), - ), - TextSpan( - text: ' مزايا', - style: TextStyle( - fontSize: 16, - fontFamily: AppState().isArabic(context) ? 'Cairo' : 'Poppins', - fontWeight: FontWeight.w700, - letterSpacing: -0.2, - height: 24 / 16, - color: MyColors.mazayaRedColor, // Use your MAZAYA red color here if defined, e.g. MyColors.mazayaRed - ), - ), - ], - ) - : TextSpan( - children: [ - TextSpan( - text: LocaleKeys.explore.tr() + ' ', - style: const TextStyle( - fontSize: 16, - letterSpacing: -0.2, - fontFamily: 'Poppins', - fontWeight: FontWeight.w700, - height: 24 / 16, - color: Color(0xFF5D5E5E), - ), - ), - TextSpan( - text: LocaleKeys.mazaya.tr(), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, - fontFamily: 'Poppins', - letterSpacing: -0.2, - height: 24 / 16, - color: MyColors.mazayaRedColor, // Use your MAZAYA red color here if defined, e.g. MyColors.mazayaRed - ), - ), - TextSpan( - text: ' ' + LocaleKeys.benefits.tr(), - style: const TextStyle( - fontSize: 16, - letterSpacing: -0.2, - fontFamily: 'Poppins', - fontWeight: FontWeight.w700, - height: 24 / 16, - color: Color(0xFF5D5E5E), - ), - ), - ], - ), + AppState().isArabic(context) + ? TextSpan( + children: [ + TextSpan( + text: 'اطلع على مميزات', + style: TextStyle( + fontSize: 16, + letterSpacing: -0.2, + fontFamily: AppState().isArabic(context) ? 'Cairo' : 'Poppins', + fontWeight: FontWeight.w700, + height: 24 / 16, + color: Color(0xFF5D5E5E), + ), + ), + TextSpan( + text: ' مزايا', + style: TextStyle( + fontSize: 16, + fontFamily: AppState().isArabic(context) ? 'Cairo' : 'Poppins', + fontWeight: FontWeight.w700, + letterSpacing: -0.2, + height: 24 / 16, + color: MyColors.mazayaRedColor, // Use your MAZAYA red color here if defined, e.g. MyColors.mazayaRed + ), + ), + ], + ) + : TextSpan( + children: [ + TextSpan( + text: LocaleKeys.explore.tr() + ' ', + style: const TextStyle( + fontSize: 16, + letterSpacing: -0.2, + fontFamily: 'Poppins', + fontWeight: FontWeight.w700, + height: 24 / 16, + color: Color(0xFF5D5E5E), + ), + ), + TextSpan( + text: LocaleKeys.mazaya.tr(), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + fontFamily: 'Poppins', + letterSpacing: -0.2, + height: 24 / 16, + color: MyColors.mazayaRedColor, // Use your MAZAYA red color here if defined, e.g. MyColors.mazayaRed + ), + ), + TextSpan( + text: ' ' + LocaleKeys.benefits.tr(), + style: const TextStyle( + fontSize: 16, + letterSpacing: -0.2, + fontFamily: 'Poppins', + fontWeight: FontWeight.w700, + height: 24 / 16, + color: Color(0xFF5D5E5E), + ), + ), + ], + ), ), ), const Expanded(flex: 1, child: SizedBox()), @@ -576,12 +576,10 @@ class _DashboardScreenState extends State with WidgetsBindingOb LocaleKeys.discounts.tr().toText24(isBold: true), 6.width, Container( - padding: const EdgeInsets.only(left: 8, right: 8), - decoration: BoxDecoration( - color: MyColors.yellowColor, - borderRadius: BorderRadius.circular(10), - ), - child: LocaleKeys.newString.tr().toText10(isBold: true)), + padding: const EdgeInsets.only(left: 8, right: 8), + decoration: BoxDecoration(color: MyColors.yellowColor, borderRadius: BorderRadius.circular(10)), + child: LocaleKeys.newString.tr().toText10(isBold: true), + ), ], ), ], @@ -589,7 +587,7 @@ class _DashboardScreenState extends State with WidgetsBindingOb ), LocaleKeys.viewAllOffers.tr().toText12(isUnderLine: true).onPress(() { Navigator.pushNamed(context, AppRoutes.offersAndDiscounts); - }) + }), ], ).paddingOnly(left: 21, right: 21), Consumer( @@ -597,59 +595,54 @@ class _DashboardScreenState extends State with WidgetsBindingOb return SizedBox( height: 103 + 33, child: ListView.separated( - shrinkWrap: true, - physics: const BouncingScrollPhysics(), - padding: const EdgeInsets.only(left: 21, right: 21, top: 13), - scrollDirection: Axis.horizontal, - itemBuilder: (BuildContext cxt, int index) { - return model.isOffersLoading - ? const OffersShimmerWidget() - : InkWell( - onTap: () { - navigateToDetails(data.getOffersList[index]); - }, - child: SizedBox( - width: 73, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 73, - height: 73, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all( - Radius.circular(100), - ), - border: Border.all(color: MyColors.lightGreyE3Color, width: 1), - ), - child: ClipRRect( - borderRadius: const BorderRadius.all( - Radius.circular(50), + shrinkWrap: true, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(left: 21, right: 21, top: 13), + scrollDirection: Axis.horizontal, + itemBuilder: (BuildContext cxt, int index) { + return model.isOffersLoading + ? const OffersShimmerWidget() + : InkWell( + onTap: () { + navigateToDetails(data.getOffersList[index]); + }, + child: SizedBox( + width: 73, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 73, + height: 73, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(100)), + border: Border.all(color: MyColors.lightGreyE3Color, width: 1), ), - child: Hero( - tag: "ItemImage" + data.getOffersList[index].offersDiscountId.toString()!, - transitionOnUserGestures: true, - child: Image.network( - data.getOffersList[index].logo ?? "", - fit: BoxFit.contain, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(50)), + child: Hero( + tag: "ItemImage" + data.getOffersList[index].offersDiscountId.toString()!, + transitionOnUserGestures: true, + child: Image.network(data.getOffersList[index].logo ?? "", fit: BoxFit.contain), ), ), ), - ), - 4.height, - Expanded( - child: AppState().isArabic(context) - ? data.getOffersList[index].titleAr!.toText12(isCenter: true, maxLine: 1) - : data.getOffersList[index].titleEn!.toText12(isCenter: true, maxLine: 1), - ), - ], + 4.height, + Expanded( + child: + AppState().isArabic(context) + ? data.getOffersList[index].titleAr!.toText12(isCenter: true, maxLine: 1) + : data.getOffersList[index].titleEn!.toText12(isCenter: true, maxLine: 1), + ), + ], + ), ), - ), - ); - }, - separatorBuilder: (BuildContext cxt, int index) => 8.width, - itemCount: 9), + ); + }, + separatorBuilder: (BuildContext cxt, int index) => 8.width, + itemCount: 9, + ), ); }, ), @@ -772,28 +765,28 @@ class _DashboardScreenState extends State with WidgetsBindingOb SvgPicture.asset( "assets/icons/chat/chat.svg", color: - !checkIfPrivilegedForChat() - ? MyColors.lightGreyE3Color - : currentIndex == 4 - ? MyColors.grey3AColor - : cProvider.disbaleChatForThisUser - ? MyColors.lightGreyE3Color - : MyColors.grey98Color, + !checkIfPrivilegedForChat() + ? MyColors.lightGreyE3Color + : currentIndex == 4 + ? MyColors.grey3AColor + : cProvider.disbaleChatForThisUser + ? MyColors.lightGreyE3Color + : MyColors.grey98Color, ).paddingAll(4), Consumer( builder: (BuildContext cxt, ChatProviderModel data, Widget? child) { return !checkIfPrivilegedForChat() ? const SizedBox() : Positioned( - right: 0, - top: 0, - child: Container( - padding: const EdgeInsets.only(left: 4, right: 4), - alignment: Alignment.center, - decoration: BoxDecoration(color: cProvider.disbaleChatForThisUser ? MyColors.pinkDarkColor : MyColors.redColor, borderRadius: BorderRadius.circular(17)), - child: data.chatUConvCounter.toString().toText10(color: Colors.white), - ), - ); + right: 0, + top: 0, + child: Container( + padding: const EdgeInsets.only(left: 4, right: 4), + alignment: Alignment.center, + decoration: BoxDecoration(color: cProvider.disbaleChatForThisUser ? MyColors.pinkDarkColor : MyColors.redColor, borderRadius: BorderRadius.circular(17)), + child: data.chatUConvCounter.toString().toText10(color: Colors.white), + ), + ); }, ), ], @@ -824,6 +817,19 @@ class _DashboardScreenState extends State with WidgetsBindingOb }, ), ), + floatingActionButton: FloatingActionButton.large( + onPressed: () { + Navigator.pushNamed(context, AppRoutes.chatBotHome); + }, + backgroundColor: Colors.transparent, + elevation: 0, + focusElevation: 0, + hoverElevation: 0, + highlightElevation: 0, + + shape: const CircleBorder(), + child: SvgPicture.asset("assets/icons/chatbot.svg", width: 100, height: 100), + ), ), ); } @@ -864,4 +870,4 @@ class _DashboardScreenState extends State with WidgetsBindingOb } return false; } -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index 6abe6fc..1234352 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -142,6 +142,8 @@ dependencies: webview_flutter: ^4.13.0 nfc_manager: ^3.2.0 + speech_to_text: ^7.3.0 + # saf: ^1.0.3+4