From f253a5e99e9097546a228bbf4f0cb56ef5920b06 Mon Sep 17 00:00:00 2001 From: aamir-csol Date: Wed, 31 Dec 2025 16:42:40 +0300 Subject: [PATCH] Chat Bot Completed --- lib/ui/chatbot/chatbot_screen.dart | 184 +++++++++++++++++++++++------ 1 file changed, 150 insertions(+), 34 deletions(-) diff --git a/lib/ui/chatbot/chatbot_screen.dart b/lib/ui/chatbot/chatbot_screen.dart index 5b5989d..dca43fa 100644 --- a/lib/ui/chatbot/chatbot_screen.dart +++ b/lib/ui/chatbot/chatbot_screen.dart @@ -31,6 +31,7 @@ class _ChatBotScreenState extends State { final TextEditingController _messageController = TextEditingController(); final ScrollController _scrollController = ScrollController(); AtlasInitiateChatResponse? atlasChatTokenResponse; + bool _isThinking = false; // Speech to text final SpeechToText _speechToText = SpeechToText(); @@ -40,7 +41,6 @@ class _ChatBotScreenState extends State { void initState() { // TODO: implement initState super.initState(); - print("Chat Bot Launched"); getChatSession(); _initSpeech(); } @@ -76,7 +76,9 @@ class _ChatBotScreenState extends State { try { GenericResponseModel? res = await DashboardApiClient().getChatBotSession(); if (res != null) { - atlasChatTokenResponse = res.atlasInitiateChatResponse; + setState(() { + atlasChatTokenResponse = res.atlasInitiateChatResponse; + }); } } catch (ex) { Utils.handleException(ex, context, null); @@ -92,37 +94,23 @@ class _ChatBotScreenState extends State { super.dispose(); } - void _sendMessage() async { - String text = _messageController.text.trim(); + void _sendMessage({String? suggestionText}) async { + String text = suggestionText ?? _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(); + _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 ?? "", @@ -135,6 +123,9 @@ class _ChatBotScreenState extends State { _scrollToBottom(); } } catch (ex) { + setState(() { + _isThinking = false; + }); Utils.handleException(ex, context, null); } @@ -150,8 +141,11 @@ class _ChatBotScreenState extends State { } void _sendSuggestion(String text) { - _messageController.text = text; - _sendMessage(); + if (atlasChatTokenResponse == null) { + Utils.showToast("Please wait, initializing chat..."); + return; + } + _sendMessage(suggestionText: text); } @override @@ -199,8 +193,11 @@ class _ChatBotScreenState extends State { return ListView.separated( controller: _scrollController, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - itemCount: _messages.length, + 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); }, @@ -210,9 +207,28 @@ class _ChatBotScreenState extends State { ); } + 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; - return Row( + Widget bubbleContent = Row( mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -251,6 +267,11 @@ class _ChatBotScreenState extends State { if (!isUser) const Spacer(flex: 2), ], ); + + if (!isUser) { + return AnimatedMessageBubble(child: bubbleContent); + } + return bubbleContent; } String _formatTimestamp(DateTime timestamp) { @@ -300,21 +321,25 @@ class _ChatBotScreenState extends State { Expanded( child: TextField( controller: _messageController, - onSubmitted: (_) => _sendMessage(), + onSubmitted: _isThinking ? null : (_) => _sendMessage(), + enabled: !_isThinking, maxLines: 1, textAlignVertical: TextAlignVertical.center, - decoration: const InputDecoration( - hintText: 'Type a message..', - hintStyle: TextStyle(color: MyColors.hintTextColor, fontSize: 14, fontWeight: FontWeight.normal), + 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: EdgeInsets.symmetric(vertical: 10), + contentPadding: const 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)), + 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), + ), ), ], ), @@ -323,8 +348,13 @@ class _ChatBotScreenState extends State { 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"))), + 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)), + ), ), ], ), @@ -332,3 +362,89 @@ class _ChatBotScreenState extends State { ).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)), + ); + } +}