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; bool _isThinking = false; // Speech to text final SpeechToText _speechToText = SpeechToText(); bool _speechEnabled = false; @override void initState() { // TODO: implement initState super.initState(); 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) { setState(() { atlasChatTokenResponse = res.atlasInitiateChatResponse; }); } } catch (ex) { Utils.handleException(ex, context, null); } } @override void dispose() { atlasChatTokenResponse = null; _messageController.dispose(); _scrollController.dispose(); super.dispose(); } void _sendMessage({String? suggestionText}) async { String text = suggestionText ?? _messageController.text.trim(); if (text.isEmpty || atlasChatTokenResponse == null) return; try { setState(() { _messages.add(ChatMessage(text: text, isUser: true, timestamp: DateTime.now())); _currentMode = ChatMode.chatActive; _messageController.clear(); _isThinking = true; }); _scrollToBottom(); GenericResponseModel? res = await DashboardApiClient().sendChatBotMessage(atlasChatTokenResponse: atlasChatTokenResponse!, atlasChatText: text); if (res != null) { res.atlasContinueChatResponseDetails; if (mounted) { setState(() { _isThinking = false; _messages.add( ChatMessage( text: res.atlasContinueChatResponseDetails!.aiResponse!.structuredData!.answer ?? "", isUser: false, timestamp: res.atlasContinueChatResponseDetails!.aiResponse!.createdAt ?? DateTime.now(), ), ); }); } _scrollToBottom(); } } catch (ex) { setState(() { _isThinking = false; }); 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) { if (atlasChatTokenResponse == null) { Utils.showToast("Please wait, initializing chat..."); return; } _sendMessage(suggestionText: text); } @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 + (_isThinking ? 1 : 0), itemBuilder: (BuildContext context, int index) { if (index == _messages.length && _isThinking) { return _buildThinkingIndicator(); } ChatMessage message = _messages[index]; return _buildMessageBubble(message); }, separatorBuilder: (BuildContext context, int index) { return const SizedBox(height: 15); }, ); } Widget _buildThinkingIndicator() { return Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( gradient: const LinearGradient(colors: [Color(0xFF32D892), Color(0xFF259CB8)], begin: Alignment.centerLeft, end: Alignment.centerRight), borderRadius: BorderRadius.circular(15), boxShadow: [BoxShadow(color: const Color(0xFF000000).withValues(alpha: 0.0784), offset: const Offset(0, 0), blurRadius: 13, spreadRadius: 0)], ), child: const ThinkingDotsAnimation(), ), const Spacer(flex: 2), ], ); } Widget _buildMessageBubble(ChatMessage message) { bool isUser = message.isUser; Widget bubbleContent = 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), ], ); if (!isUser) { return AnimatedMessageBubble(child: bubbleContent); } return bubbleContent; } 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: _isThinking ? null : (_) => _sendMessage(), enabled: !_isThinking, maxLines: 1, textAlignVertical: TextAlignVertical.center, decoration: InputDecoration( hintText: _isThinking ? 'Waiting for response...' : 'Type a message..', hintStyle: const TextStyle(color: MyColors.hintTextColor, fontSize: 14, fontWeight: FontWeight.normal), border: InputBorder.none, isDense: true, contentPadding: const EdgeInsets.symmetric(vertical: 10), ), ), ), GestureDetector( onTap: _isThinking ? null : (_speechEnabled ? (_speechToText.isNotListening ? _startListening : _stopListening) : null), child: SvgPicture.asset( "assets/icons/microphone.svg", colorFilter: ColorFilter.mode(_isThinking ? MyColors.hintTextColor : (_speechToText.isListening ? Colors.red : MyColors.darkTextColor), BlendMode.srcIn), ), ), ], ), ), ), const SizedBox(width: 6), // Send button GestureDetector( onTap: _isThinking ? null : _sendMessage, child: Container( width: 48, height: 48, decoration: BoxDecoration(color: _isThinking ? MyColors.hintTextColor.withValues(alpha: 0.3) : MyColors.white, shape: BoxShape.circle), child: Center(child: SvgPicture.asset("assets/icons/send.svg", colorFilter: _isThinking ? const ColorFilter.mode(MyColors.hintTextColor, BlendMode.srcIn) : null)), ), ), ], ), ), ).paddingOnly(left: 16, right: 16, bottom: 16); } } class ThinkingDotsAnimation extends StatefulWidget { const ThinkingDotsAnimation({Key? key}) : super(key: key); @override State createState() => _ThinkingDotsAnimationState(); } class _ThinkingDotsAnimationState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1200))..repeat(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Row( mainAxisSize: MainAxisSize.min, children: [ const Text("Thinking", style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white)), const SizedBox(width: 2), AnimatedBuilder( animation: _controller, builder: (BuildContext context, Widget? child) { int dotCount = (_controller.value * 4).floor() % 4; String dots = '.' * dotCount; return SizedBox(width: 18, child: Text(dots, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white))); }, ), ], ); } } class AnimatedMessageBubble extends StatefulWidget { final Widget child; const AnimatedMessageBubble({Key? key, required this.child}) : super(key: key); @override State createState() => _AnimatedMessageBubbleState(); } class _AnimatedMessageBubbleState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _fadeAnimation; late Animation _slideAnimation; late Animation _scaleAnimation; @override void initState() { super.initState(); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 400)); _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut)); _slideAnimation = Tween(begin: const Offset(-0.3, 0.0), end: Offset.zero).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic)); _scaleAnimation = Tween(begin: 0.8, end: 1.0).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutBack)); _controller.forward(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return SlideTransition( position: _slideAnimation, child: FadeTransition(opacity: _fadeAnimation, child: ScaleTransition(scale: _scaleAnimation, alignment: Alignment.centerLeft, child: widget.child)), ); } }