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.
335 lines
12 KiB
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);
|
|
}
|
|
}
|