didvan-app/lib/views/widgets/ai_chat_dialog.dart

928 lines
29 KiB
Dart

import 'package:didvan/services/ai_rag_service.dart';
import 'package:didvan/services/ai_voice_service.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/providers/user.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:provider/provider.dart';
import 'package:record/record.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:ui';
import 'dart:io';
class AiChatDialog extends StatefulWidget {
const AiChatDialog({super.key});
@override
State<AiChatDialog> createState() => _AiChatDialogState();
}
class _AiChatDialogState extends State<AiChatDialog>
with TickerProviderStateMixin {
final TextEditingController _messageController = TextEditingController();
final ScrollController _scrollController = ScrollController();
final List<ChatMessage> _messages = [];
bool _isLoading = false;
bool _isRecording = false;
late AnimationController _animationController;
late AnimationController _pulseController;
final AudioRecorder _audioRecorder = AudioRecorder();
String? _recordingPath;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
_pulseController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat(reverse: true);
_animationController.forward();
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
setState(() {
_messages.add(ChatMessage(
text:
'سلام! 👋\n\nمن دستیار هوشمند دیدوان هستم. می‌تونم در مورد اخبار، تحلیل‌ها و محتوای دیدوان بهتون کمک کنم.\n\nچه سوالی دارید؟ 😊',
isUser: false,
timestamp: DateTime.now(),
));
});
_scrollToBottom();
}
});
}
@override
void dispose() {
_messageController.dispose();
_scrollController.dispose();
_animationController.dispose();
_pulseController.dispose();
_audioRecorder.dispose();
super.dispose();
}
void _scrollToBottom() {
Future.delayed(const Duration(milliseconds: 100), () {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
Future<void> _sendMessage() async {
final message = _messageController.text.trim();
if (message.isEmpty) return;
setState(() {
_messages.add(ChatMessage(
text: message,
isUser: true,
timestamp: DateTime.now(),
));
_isLoading = true;
_messageController.clear();
});
_scrollToBottom();
final response = await AiRagService.sendMessage(message);
setState(() {
_messages.add(ChatMessage(
text: response.output,
isUser: false,
timestamp: DateTime.now(),
sources: response.sources,
));
_isLoading = false;
});
_scrollToBottom();
}
Future<void> _startRecording() async {
try {
if (await _audioRecorder.hasPermission()) {
setState(() {
_isRecording = true;
});
final directory = await getTemporaryDirectory();
final timestamp = DateTime.now().millisecondsSinceEpoch;
_recordingPath = '${directory.path}/voice_$timestamp.m4a';
await _audioRecorder.start(
const RecordConfig(
encoder: AudioEncoder.aacLc,
),
path: _recordingPath!,
);
await Future.delayed(const Duration(milliseconds: 200));
}
} catch (e) {
setState(() {
_isRecording = false;
});
debugPrint('Error starting recording: $e');
}
}
Future<void> _stopRecording() async {
try {
final path = await _audioRecorder.stop();
setState(() {
_isRecording = false;
});
if (path != null) {
setState(() {
_messages.add(ChatMessage(
text: '🎤 پیام صوتی',
isUser: true,
timestamp: DateTime.now(),
));
_isLoading = true;
});
_scrollToBottom();
final response = await AiVoiceService.uploadVoice(path);
if (response.isSuccess && response.text.isNotEmpty) {
final ragResponse = await AiRagService.sendMessage(response.text);
setState(() {
_messages.add(ChatMessage(
text: ragResponse.output,
isUser: false,
timestamp: DateTime.now(),
sources: ragResponse.sources,
));
_isLoading = false;
});
} else {
setState(() {
_messages.add(ChatMessage(
text: 'خطا در پردازش پیام صوتی',
isUser: false,
timestamp: DateTime.now(),
));
_isLoading = false;
});
}
_scrollToBottom();
try {
await File(path).delete();
} catch (e) {
debugPrint('Error deleting temp file: $e');
}
}
} catch (e) {
setState(() {
_isRecording = false;
_isLoading = false;
});
debugPrint('Error stopping recording: $e');
}
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: ScaleTransition(
scale: CurvedAnimation(
parent: _animationController,
curve: Curves.elasticOut,
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 600),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withOpacity(0.95),
const Color(0xFFF8F9FF).withOpacity(0.95),
],
),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: Colors.white.withOpacity(0.6),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF0066AA).withOpacity(0.15),
blurRadius: 30,
spreadRadius: 0,
offset: const Offset(0, 15),
),
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 15,
spreadRadius: -3,
offset: const Offset(0, 8),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Column(
children: [
_buildHeader(context),
Expanded(
child: _buildMessageList(),
),
if (_isLoading) _buildLoadingIndicator(),
if (_isRecording) _buildRecordingIndicator(),
_buildInputField(),
],
),
),
),
),
),
);
}
Widget _buildHeader(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 14),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0066AA),
Color(0xFF0088DD),
Color(0xFF00AAFF),
],
),
boxShadow: [
BoxShadow(
color: const Color(0xFF0066AA).withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 3),
),
],
),
child: Row(
children: [
Stack(
children: [
AnimatedBuilder(
animation: _pulseController,
builder: (context, child) {
return Container(
width: 44,
height: 44,
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.white
.withOpacity(0.25 * _pulseController.value),
blurRadius: 15 + (8 * _pulseController.value),
spreadRadius: 1 + (2 * _pulseController.value),
),
],
),
);
},
),
// Avatar
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
padding: const EdgeInsets.all(10),
child: SvgPicture.asset(
'lib/assets/icons/live ai.svg',
colorFilter: const ColorFilter.mode(
Color(0xFF0066AA),
BlendMode.srcIn,
),
),
),
// Online indicator
Positioned(
bottom: 1,
left: 1,
child: Container(
width: 11,
height: 11,
decoration: BoxDecoration(
color: const Color(0xFF00FF88),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1.5),
boxShadow: [
BoxShadow(
color: const Color(0xFF00FF88).withOpacity(0.4),
blurRadius: 6,
spreadRadius: 0.5,
),
],
),
),
),
],
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
'دستیار هوشمند دیدوان',
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
SizedBox(height: 3),
Row(
children: [
SizedBox(width: 4),
DidvanText(
'آنلاین',
fontSize: 11,
color: Colors.white70,
),
],
),
],
),
),
Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: IconButton(
icon: const Icon(Icons.close_rounded,
color: Colors.white, size: 20),
onPressed: () => Navigator.pop(context),
splashRadius: 20,
),
),
],
),
);
}
Widget _buildMessageList() {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white.withOpacity(0.5),
const Color(0xFFF8F9FF).withOpacity(0.3),
],
),
),
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.fromLTRB(14, 14, 14, 10),
itemCount: _messages.length,
itemBuilder: (context, index) {
final message = _messages[index];
return _buildMessageBubble(message);
},
),
);
}
Widget _buildMessageBubble(ChatMessage message) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 400),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
return Transform.translate(
offset: Offset(0, 20 * (1 - value)),
child: Opacity(
opacity: value,
child: child,
),
);
},
child: Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
mainAxisAlignment:
message.isUser ? MainAxisAlignment.start : MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (message.isUser) _buildUserAvatar(),
if (message.isUser) const SizedBox(width: 8),
Flexible(
child: Column(
crossAxisAlignment: message.isUser
? CrossAxisAlignment.start
: CrossAxisAlignment.end,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 10),
decoration: BoxDecoration(
gradient: !message.isUser
? const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0066AA),
Color(0xFF0088DD),
],
)
: null,
color: !message.isUser ? null : const Color(0xFFF5F7FA),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(!message.isUser ? 4 : 16),
topRight: const Radius.circular(16),
bottomLeft: const Radius.circular(16),
bottomRight: Radius.circular(!message.isUser ? 16 : 4),
),
border: !message.isUser
? null
: Border.all(
color: const Color(0xFFE0E5EC).withOpacity(0.5),
width: 1,
),
boxShadow: [
BoxShadow(
color: !message.isUser
? const Color(0xFF0066AA).withOpacity(0.2)
: Colors.black.withOpacity(0.03),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: DidvanText(
message.text,
color: !message.isUser
? Colors.white
: const Color(0xFF1A1A1A),
fontSize: 13.5,
),
),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 3),
child: DidvanText(
_formatTime(message.timestamp),
fontSize: 10,
color: Colors.grey.shade400,
),
),
if (message.sources.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFF0066AA).withOpacity(0.08),
const Color(0xFF0088DD).withOpacity(0.05),
],
),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: const Color(0xFF0066AA).withOpacity(0.2),
width: 0.8,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: const Color(0xFF0066AA).withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.bookmark_rounded,
size: 12,
color: Color(0xFF0066AA),
),
),
const SizedBox(width: 6),
DidvanText(
'منابع: ${message.sources.join(", ")}',
fontSize: 11,
color: const Color(0xFF0066AA),
fontWeight: FontWeight.w600,
),
],
),
),
],
],
),
),
if (!message.isUser) const SizedBox(width: 8),
if (!message.isUser) _buildAiAvatar(),
],
),
),
);
}
Widget _buildAiAvatar() {
return Container(
width: 32,
height: 32,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0066AA),
Color(0xFF00AAFF),
],
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: const Color(0xFF0066AA).withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
padding: const EdgeInsets.all(7),
child: SvgPicture.asset(
'lib/assets/icons/live ai.svg',
colorFilter: const ColorFilter.mode(
Colors.white,
BlendMode.srcIn,
),
),
);
}
Widget _buildUserAvatar() {
return Consumer<UserProvider>(
builder: (context, userProvider, _) {
if (userProvider.user.photo != null &&
userProvider.user.photo!.isNotEmpty) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.25),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
userProvider.user.photo!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildDefaultUserAvatar();
},
),
),
);
}
return _buildDefaultUserAvatar();
},
);
}
Widget _buildDefaultUserAvatar() {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.grey.shade400,
Colors.grey.shade500,
],
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.25),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: const Icon(
Icons.person_rounded,
size: 18,
color: Colors.white,
),
);
}
Widget _buildLoadingIndicator() {
return Padding(
padding: const EdgeInsets.fromLTRB(14, 6, 14, 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFFF5F7FA),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0xFFE0E5EC).withOpacity(0.5),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'در حال تایپ...',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
fontStyle: FontStyle.italic,
),
),
const SizedBox(width: 10),
const SpinKitThreeBounce(
color: Color(0xFF0066AA),
size: 14,
),
],
),
),
const SizedBox(width: 8),
_buildAiAvatar(),
],
),
);
}
Widget _buildRecordingIndicator() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
margin: const EdgeInsets.only(bottom: 6),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFFFF3366).withOpacity(0.1),
const Color(0xFFFF6699).withOpacity(0.05),
],
),
border: Border.all(
color: const Color(0xFFFF3366).withOpacity(0.3),
width: 1,
),
),
child: Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: const Color(0xFFFF3366),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: const Color(0xFFFF3366).withOpacity(0.4),
blurRadius: 6,
spreadRadius: 1,
),
],
),
),
const SizedBox(width: 10),
const Expanded(
child: DidvanText(
'🎙️ در حال ضبط...',
fontSize: 12,
color: Color(0xFFFF3366),
fontWeight: FontWeight.w600,
),
),
const Icon(
Icons.mic_rounded,
color: Color(0xFFFF3366),
size: 16,
),
],
),
);
}
Widget _buildInputField() {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.95),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 15,
offset: const Offset(0, -3),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Container(
constraints: const BoxConstraints(maxHeight: 100),
decoration: BoxDecoration(
color: const Color(0xFFF5F7FA),
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: const Color(0xFFE0E5EC).withOpacity(0.5),
width: 1.2,
),
),
child: TextField(
controller: _messageController,
maxLines: null,
textInputAction: TextInputAction.send,
onSubmitted: (_) => _sendMessage(),
style: const TextStyle(
fontSize: 13.5,
color: Color(0xFF1A1A1A),
),
decoration: InputDecoration(
hintText: 'پیام خود را بنویسید...',
hintStyle: TextStyle(
color: Colors.grey.shade400,
fontSize: 13,
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
),
),
),
const SizedBox(width: 10),
GestureDetector(
onTap: () {
if (!_isLoading) {
if (_isRecording) {
_stopRecording();
} else {
_startRecording();
}
}
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: _isRecording
? [const Color(0xFFFF3366), const Color(0xFFFF6699)]
: [const Color(0xFF6B7280), const Color(0xFF9CA3AF)],
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: (_isRecording
? const Color(0xFFFF3366)
: const Color(0xFF6B7280))
// ignore: deprecated_member_use
.withOpacity(0.35),
blurRadius: _isRecording ? 16 : 10,
offset: const Offset(0, 3),
),
],
),
child: Icon(
_isRecording ? Icons.stop_rounded : Icons.mic_rounded,
color: Colors.white,
size: 20,
),
),
),
const SizedBox(width: 10),
// دکمه ارسال
GestureDetector(
onTap: _isLoading || _isRecording ? null : _sendMessage,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: (_isLoading || _isRecording)
? [Colors.grey.shade300, Colors.grey.shade400]
: [const Color(0xFF0066AA), const Color(0xFF00AAFF)],
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: ((_isLoading || _isRecording)
? Colors.grey.shade400
: const Color(0xFF0066AA))
// ignore: deprecated_member_use
.withOpacity(0.35),
blurRadius: 12,
offset: const Offset(0, 3),
),
],
),
child: _isLoading
? const Padding(
padding: EdgeInsets.all(11),
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Icon(
Icons.arrow_upward_rounded,
color: Colors.white,
size: 20,
),
),
),
],
),
);
}
String _formatTime(DateTime time) {
final hour = time.hour.toString().padLeft(2, '0');
final minute = time.minute.toString().padLeft(2, '0');
return '$hour:$minute';
}
}
class ChatMessage {
final String text;
final bool isUser;
final DateTime timestamp;
final List<int> sources;
ChatMessage({
required this.text,
required this.isUser,
required this.timestamp,
this.sources = const [],
});
}