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

972 lines
34 KiB
Dart

// ignore_for_file: deprecated_member_use
import 'package:didvan/services/ai_rag_service.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/ai_voice_chat_dialog.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/views/widgets/glass/liquid_glass_container.dart';
import 'package:didvan/views/widgets/glass/glass_logic.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:just_audio/just_audio.dart';
import 'dart:ui';
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;
late AnimationController _shimmerController;
final AudioRecorder _audioRecorder = AudioRecorder();
final AudioPlayer _audioPlayer = AudioPlayer();
String? _recordingPath;
String? _currentPlayingUrl;
bool _isPlaying = false;
final GlassShader _headerGlassShader = GlassShader();
final GlassShader _inputGlassShader = GlassShader();
final GlassShader _loadingGlassShader = GlassShader();
final GlobalKey _backgroundKey = GlobalKey();
@override
void initState() {
super.initState();
Future.wait([
_headerGlassShader.initialize(),
_inputGlassShader.initialize(),
_loadingGlassShader.initialize(),
] as Iterable<Future>).then((_) {
if (mounted) setState(() {});
});
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
);
_audioPlayer.playerStateStream.listen((state) {
if (mounted) {
setState(() {
_isPlaying = state.playing;
if (state.processingState == ProcessingState.completed) {
_isPlaying = false;
_currentPlayingUrl = null;
}
});
}
});
_pulseController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 3000),
)..repeat(reverse: true);
_shimmerController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 4000),
)..repeat();
_animationController.forward();
Future.delayed(const Duration(milliseconds: 600), () {
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();
_shimmerController.dispose();
_audioRecorder.dispose();
_audioPlayer.dispose();
super.dispose();
}
void _scrollToBottom() {
Future.delayed(const Duration(milliseconds: 100), () {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 600),
curve: Curves.easeOutCubic,
);
}
});
}
Future<void> _toggleAudioPlayback(String audioUrl) async {
try {
if (_isPlaying && _currentPlayingUrl == audioUrl) {
await _audioPlayer.pause();
setState(() {
_isPlaying = false;
});
} else {
if (_currentPlayingUrl != audioUrl) {
final token = RequestService.token;
AudioSource? audioSource;
try {
audioSource = AudioSource.uri(
Uri.parse(audioUrl),
headers: {'Authorization': 'Bearer $token'},
);
await _audioPlayer.setAudioSource(audioSource);
setState(() {
_currentPlayingUrl = audioUrl;
});
} catch (headerError) {
await _audioPlayer.setUrl(audioUrl);
setState(() {
_currentPlayingUrl = audioUrl;
});
}
}
await _audioPlayer.play();
setState(() {
_isPlaying = true;
});
}
} catch (e) {
debugPrint('❌ Error playing audio: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('خطا در پخش صوت')),
);
}
}
}
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,
audioUrl: response.audioUrl,
));
_isLoading = false;
});
_scrollToBottom();
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.zero,
child: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
color: Colors.black.withOpacity(0.01),
),
),
),
),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: ScaleTransition(
scale: CurvedAnimation(
parent: _animationController,
curve: Curves.elasticOut,
),
child: Container(
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 680),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(40),
color: Colors.white.withOpacity(0.05),
boxShadow: [
BoxShadow(
color: const Color(0xFF0066AA).withOpacity(0.15),
blurRadius: 40,
spreadRadius: 0,
offset: const Offset(0, 20),
),
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
padding: const EdgeInsets.all(1.5),
child: ClipRRect(
borderRadius: BorderRadius.circular(39),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Stack(
children: [
RepaintBoundary(
key: _backgroundKey,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(39),
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color.fromARGB(55, 255, 255, 255),
Color.fromARGB(50, 255, 255, 255),
],
),
),
child: Stack(
children: [
Positioned.fill(
child: AnimatedBuilder(
animation: _shimmerController,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment(
-1.5 + (_shimmerController.value * 2.5),
-0.5),
end: Alignment(
0.5 + (_shimmerController.value * 2.5),
1.5),
colors: [
Colors.transparent,
Colors.white.withOpacity(0.1),
Colors.transparent,
],
stops: const [0.0, 0.5, 1.0],
),
),
);
},
),
),
Positioned.fill(
child: Stack(
alignment: Alignment.bottomCenter,
children: [
_buildMessageList(),
],
),
),
],
),
),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: LiquidGlassContainer(
backgroundKey: _backgroundKey,
shader: _headerGlassShader,
distortion: 6.0,
blur: 30.0,
dispersion: 10.0,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(39),
topRight: Radius.circular(39),
),
child: _buildHeaderContent(context),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: LiquidGlassContainer(
backgroundKey: _backgroundKey,
shader: _inputGlassShader,
distortion: 6.0,
blur: 30.0,
dispersion: 10.0,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(39),
bottomRight: Radius.circular(39),
),
child: _buildInputFieldContent(),
),
),
],
),
),
),
),
),
),
),
],
),
);
}
Widget _buildHeaderContent(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
decoration: BoxDecoration(
color: const Color.fromARGB(150, 255, 255, 255),
border: Border(
bottom: BorderSide(
color: Colors.white.withOpacity(0.2),
width: 1,
),
),
),
child: Row(
children: [
Container(
height: 48,
width: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withOpacity(0.9),
const Color(0xFFE0F7FF),
const Color(0xFFF0FDFF),
],
stops: const [0.0, 0.3, 1.0],
),
border: Border.all(
color: Colors.white,
width: 1.5,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF0066AA).withOpacity(0.2),
blurRadius: 15,
offset: const Offset(0, 8),
),
BoxShadow(
color: Colors.white.withOpacity(0.5),
blurRadius: 10,
offset: const Offset(-2, -2),
),
],
),
child: Padding(
padding: const EdgeInsets.all(10),
child: SvgPicture.asset(
'lib/assets/icons/live ai.svg',
colorFilter: const ColorFilter.mode(
Color(0xFF0066AA),
BlendMode.srcIn,
),
),
),
),
const SizedBox(width: 14),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
'دستیار هوشمند دیدوان',
fontSize: 13,
fontWeight: FontWeight.w800,
color: Color(0xFF1A2B3C),
),
],
),
),
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withOpacity(0.4),
),
),
child: Icon(
Icons.close_rounded,
color: Colors.grey.shade800,
size: 22,
),
),
),
],
),
);
}
Widget _buildInputFieldContent() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color.fromARGB(50, 255, 255, 255),
border: Border(
top: BorderSide(
color: Colors.white.withOpacity(0.3),
width: 1,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
GestureDetector(
onTap: _isLoading || _isRecording ? null : _sendMessage,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 52,
height: 52,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: (_isLoading || _isRecording)
? [Colors.grey.shade300, Colors.grey.shade400]
: [const Color(0xFF0066AA), const Color(0xFF0099FF)],
),
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withOpacity(0.6),
width: 2,
),
boxShadow: [
BoxShadow(
color: ((_isLoading || _isRecording)
? Colors.grey
: const Color(0xFF0066AA))
.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 6),
),
],
),
child: _isLoading
? const Padding(
padding: EdgeInsets.all(14),
child: CircularProgressIndicator(
strokeWidth: 2.5,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Padding(
padding: const EdgeInsets.all(13.0),
child: SvgPicture.asset(
'lib/assets/icons/send.svg',
color: Colors.white,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(30),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Container(
constraints: const BoxConstraints(maxHeight: 120),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.75),
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: Colors.white.withOpacity(0.5),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: TextField(
controller: _messageController,
maxLines: null,
textInputAction: TextInputAction.send,
onSubmitted: (_) => _sendMessage(),
onChanged: (_) => setState(() {}),
style: const TextStyle(
fontSize: 14,
color: Color(0xFF1A2B3C),
height: 1.5,
),
decoration: InputDecoration(
hintText: 'چیزی بنویسید...',
hintStyle: TextStyle(
color: Colors.grey.shade700,
fontSize: 11,
fontWeight: FontWeight.w500,
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
suffixIcon: _messageController.text.isEmpty
? GestureDetector(
onTap: () {
Navigator.pop(context);
showDialog(
context: context,
barrierDismissible: false,
builder: (context) =>
const AiVoiceChatDialog(),
);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0066AA),
Color(0xFF00AAFF)
],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
width: 5,
),
const DidvanText(
'چت صوتی',
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
const SizedBox(width: 6),
SvgPicture.asset(
'lib/assets/icons/voice-square.svg',
colorFilter: const ColorFilter.mode(
Colors.white,
BlendMode.srcIn,
),
height: 24,
),
],
),
),
),
),
)
: null,
suffixIconConstraints: const BoxConstraints(
minWidth: 0,
minHeight: 0,
),
),
),
),
),
),
),
],
),
);
}
Widget _buildMessageList() {
return ListView.builder(
controller: _scrollController,
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(20, 100, 20, 120),
itemCount: _messages.length + (_isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index < _messages.length) {
final message = _messages[index];
return _buildMessageBubble(message);
}
else {
return Padding(
padding: const EdgeInsets.only(bottom: 18),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildLiquidLoadingIndicator(),
],
),
);
}
},
);
}
Widget _buildMessageBubble(ChatMessage message) {
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 600),
curve: Curves.easeOutBack,
builder: (context, value, child) {
return Transform.translate(
offset: Offset(0, 30 * (1 - value)),
child: Transform.scale(
scale: 0.9 + (0.1 * value),
child: Opacity(
opacity: value,
child: child,
),
),
);
},
child: Padding(
padding: const EdgeInsets.only(bottom: 18),
child: Row(
mainAxisAlignment:
message.isUser ? MainAxisAlignment.start : MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (message.isUser) ...[
_buildUserAvatar(),
const SizedBox(width: 10),
],
Flexible(
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
decoration: BoxDecoration(
gradient: message.isUser
? null
: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0066AA),
Color(0xFF0099FF),
],
),
color: message.isUser ? Colors.white.withOpacity(0.6) : null,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(24),
topRight: const Radius.circular(24),
bottomRight: Radius.circular(message.isUser ? 4 : 24),
bottomLeft: Radius.circular(message.isUser ? 24 : 4),
),
border: Border.all(
color: message.isUser
? Colors.white.withOpacity(0.5)
: Colors.white.withOpacity(0.2),
width: 1,
),
boxShadow: [
BoxShadow(
color: message.isUser
? Colors.black.withOpacity(0.03)
: const Color(0xFF0066AA).withOpacity(0.25),
blurRadius: 15,
offset: const Offset(0, 6),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
message.text,
color:
message.isUser ? const Color(0xFF1A2B3C) : Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
const SizedBox(height: 4),
Align(
alignment: Alignment.centerLeft,
child: DidvanText(
_formatTime(message.timestamp),
fontSize: 10,
color: message.isUser
? Colors.grey.shade600
: Colors.white.withOpacity(0.7),
),
),
if (message.sources.isNotEmpty) ...[
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: Colors.white.withOpacity(0.2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.bookmark_rounded,
size: 12,
color: Colors.white,
),
const SizedBox(width: 6),
DidvanText(
'منابع: ${message.sources.join(", ")}',
fontSize: 11,
color: Colors.white,
fontWeight: FontWeight.w600,
),
],
),
),
],
],
),
),
),
if (!message.isUser) ...[
const SizedBox(width: 10),
_buildAiAvatar(),
],
],
),
),
);
}
Widget _buildAiAvatar() {
return Container(
width: 38,
height: 38,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF0066AA), Color(0xFF00AAFF)],
),
shape: BoxShape.circle,
border: Border.all(color: Colors.white.withOpacity(0.8), width: 1.5),
boxShadow: [
BoxShadow(
color: const Color(0xFF0066AA).withOpacity(0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
padding: const EdgeInsets.all(9),
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, _) {
return Container(
width: 38,
height: 38,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withOpacity(0.8),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(19),
child: (userProvider.user.photo != null &&
userProvider.user.photo!.isNotEmpty)
? Image.network(
userProvider.user.photo!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
_buildDefaultUserAvatar(),
)
: _buildDefaultUserAvatar(),
),
);
},
);
}
Widget _buildDefaultUserAvatar() {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.grey.shade300, Colors.grey.shade400],
),
shape: BoxShape.circle,
),
child: const Icon(
Icons.person_rounded,
size: 20,
color: Colors.white,
),
);
}
Widget _buildLiquidLoadingIndicator() {
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
LiquidGlassContainer(
backgroundKey: _backgroundKey,
shader: _loadingGlassShader,
distortion: 3.0,
blur: 20.0,
dispersion: 2.2,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
bottomRight: Radius.circular(24),
bottomLeft: Radius.circular(4),
),
child: AnimatedBuilder(
animation: _shimmerController,
builder: (context, child) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: const Color.fromARGB(167, 255, 255, 255),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
bottomRight: Radius.circular(24),
bottomLeft: Radius.circular(4),
),
border: Border.all(
color: Colors.white.withOpacity(0.3),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
DidvanText(
'در حال فکر کردن...',
color: const Color(0xFF1A2B3C).withOpacity(0.8),
fontSize: 12,
fontWeight: FontWeight.w700,
),
const SizedBox(width: 10),
Stack(
alignment: Alignment.center,
children: [
SpinKitPulse(
color: const Color(0xFF0066AA).withOpacity(0.5),
size: 24,
),
const Icon(
Icons.auto_awesome,
size: 14,
color: Color(0xFF0066AA),
)
],
),
],
),
);
},
),
),
const SizedBox(width: 10),
_buildAiAvatar(),
],
);
}
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;
final String? audioUrl;
ChatMessage({
required this.text,
required this.isUser,
required this.timestamp,
this.sources = const [],
this.audioUrl,
});
}