chat bot & Speech to Text
parent
1ee51a7597
commit
234dce65e5
@ -0,0 +1,46 @@
|
||||
<svg width="74" height="74" viewBox="0 0 74 74" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_119_438)">
|
||||
<rect x="17" y="17" width="40" height="40" rx="20" fill="url(#paint0_linear_119_438)"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_d_119_438)">
|
||||
<rect x="13" y="13" width="48" height="48" rx="24" fill="url(#paint1_linear_119_438)" fill-opacity="0.4" shape-rendering="crispEdges"/>
|
||||
</g>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.9992 37.5291C40.7637 38.0552 39.4301 38.3796 38.0352 38.4637C37.7946 38.4782 37.5523 38.4856 37.3082 38.4856C37.0642 38.4856 36.8219 38.4782 36.5813 38.4637C35.1861 38.3796 33.8522 38.055 32.6165 37.5287C30.0232 36.4243 27.8623 34.4316 26.4749 31.9094C28.6373 27.9782 32.6791 25.3333 37.3082 25.3333C41.9374 25.3333 45.9792 27.9782 48.1416 31.9094C46.7541 34.4319 44.5928 36.4247 41.9992 37.5291Z" fill="white"/>
|
||||
<path d="M32.6163 38.5525C32.3175 38.4253 32.0245 38.2863 31.7378 38.136C30.8058 38.8264 30.018 39.6862 29.4248 40.6673C30.4331 42.3347 32.0037 43.6522 33.8885 44.3824C34.7866 44.7303 35.7561 44.9449 36.7701 45.0005H37.8268C38.8406 44.9449 40.6224 46.0011 38.035 48.6666C39.9201 47.9364 44.1637 42.3349 45.1721 40.6673C44.5806 39.689 43.7956 38.8313 42.8671 38.1419C42.5837 38.29 42.2941 38.4272 41.999 38.5529C40.7635 39.079 39.4299 39.4034 38.035 39.4875C37.7945 39.502 37.5521 39.5093 37.3081 39.5093C37.0641 39.5093 36.8217 39.502 36.5811 39.4875C35.1859 39.4034 33.852 39.0788 32.6163 38.5525Z" fill="white"/>
|
||||
<rect x="32.0844" y="30.2271" width="10.4705" height="3.93258" rx="1.96629" fill="#162550"/>
|
||||
<ellipse cx="40.2071" cy="32.1717" rx="0.685571" ry="0.720974" fill="#04FED1"/>
|
||||
<ellipse cx="37.2984" cy="41.8719" rx="0.685571" ry="0.720974" fill="#162550"/>
|
||||
<ellipse cx="34.5566" cy="32.1717" rx="0.685571" ry="0.720974" fill="#04FED1"/>
|
||||
<ellipse cx="34.5566" cy="41.8719" rx="0.685571" ry="0.720974" fill="#162550"/>
|
||||
<ellipse cx="40.0411" cy="41.8719" rx="0.685571" ry="0.720974" fill="#162550"/>
|
||||
<defs>
|
||||
<filter id="filter0_d_119_438" x="4" y="4" width="66" height="66" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="6.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0784314 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_119_438"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_119_438" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d_119_438" x="0" y="0" width="74" height="74" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="6.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0784314 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_119_438"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_119_438" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_119_438" x1="-12.3178" y1="86.2302" x2="-12.3178" y2="18.3971" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="1" stop-color="#259CB8"/>
|
||||
<stop stop-color="#32D892"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_119_438" x1="-22.1814" y1="96.0762" x2="-22.1814" y2="14.6765" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="1" stop-color="#259CB8"/>
|
||||
<stop stop-color="#32D892"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@ -0,0 +1,12 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_120_1007)">
|
||||
<path d="M13.125 5C13.125 3.27411 11.7259 1.875 10 1.875C8.27411 1.875 6.875 3.27411 6.875 5V10C6.875 11.7259 8.27411 13.125 10 13.125C11.7259 13.125 13.125 11.7259 13.125 10V5Z" stroke="#697586" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 15.625V18.75" stroke="#697586" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.625 10C15.625 11.4918 15.0324 12.9226 13.9775 13.9775C12.9226 15.0324 11.4918 15.625 10 15.625C8.50816 15.625 7.07742 15.0324 6.02252 13.9775C4.96763 12.9226 4.375 11.4918 4.375 10" stroke="#697586" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_120_1007">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 903 B |
@ -0,0 +1,3 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.8744 6.13098L7.58175 10.4694L2.69958 7.41575C2.00006 6.97811 2.14558 5.91558 2.93679 5.6842L14.5284 2.28958C15.253 2.07722 15.9244 2.75462 15.7092 3.4815L12.2798 15.0651C12.0448 15.8574 10.9884 15.999 10.5549 15.2965L7.5795 10.4701" stroke="#0D121C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 434 B |
@ -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<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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue