You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mohemm-flutter-app/lib/ui/chatbot/chatbot_screen.dart

335 lines
12 KiB
Dart

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<ChatBotScreen> createState() => _ChatBotScreenState();
}
class _ChatBotScreenState extends State<ChatBotScreen> {
ChatMode _currentMode = ChatMode.home;
final List<ChatMessage> _messages = <ChatMessage>[];
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: <Widget>[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: <Widget>[
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: <Widget>[
_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: <Widget>[
if (isUser) const Spacer(flex: 2),
Flexible(
flex: 7,
child: Column(
crossAxisAlignment: isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isUser ? Colors.white : null,
gradient: isUser ? null : const LinearGradient(colors: <Color>[Color(0xFF32D892), Color(0xFF259CB8)], begin: Alignment.centerLeft, end: Alignment.centerRight),
borderRadius: BorderRadius.circular(15),
boxShadow: isUser ? null : <BoxShadow>[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: <Widget>[
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: <Widget>[
// 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: <Widget>[
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);
}
}