// ignore_for_file: deprecated_member_use import 'package:didvan/services/ai_rag_service.dart'; import 'package:didvan/services/ai_voice_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: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 'package:just_audio/just_audio.dart'; import 'dart:ui'; import 'dart:io'; class AiChatDialog extends StatefulWidget { const AiChatDialog({super.key}); @override State createState() => _AiChatDialogState(); } class _AiChatDialogState extends State with TickerProviderStateMixin { final TextEditingController _messageController = TextEditingController(); final ScrollController _scrollController = ScrollController(); final List _messages = []; bool _isLoading = false; bool _isRecording = false; late AnimationController _animationController; late AnimationController _pulseController; final AudioRecorder _audioRecorder = AudioRecorder(); final AudioPlayer _audioPlayer = AudioPlayer(); String? _recordingPath; String? _currentPlayingUrl; bool _isPlaying = false; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 600), ); _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: 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(); _audioPlayer.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 _toggleAudioPlayback(String audioUrl) async { try { if (_isPlaying && _currentPlayingUrl == audioUrl) { await _audioPlayer.pause(); setState(() { _isPlaying = false; }); } else { if (_currentPlayingUrl != audioUrl) { final token = RequestService.token; debugPrint('🎵 Audio URL: $audioUrl'); debugPrint('🔑 Token exists: ${token != null && token.isNotEmpty}'); AudioSource? audioSource; try { if (token != null && token.isNotEmpty) { audioSource = AudioSource.uri( Uri.parse(audioUrl), headers: { 'Authorization': 'Bearer $token', }, ); debugPrint('✅ Trying with token (no Bearer prefix)'); } else { audioSource = AudioSource.uri( Uri.parse(audioUrl), headers: { 'Authorization': 'Bearer $token', }, ); debugPrint('✅ Trying without Authorization header'); } await _audioPlayer.setAudioSource(audioSource); setState(() { _currentPlayingUrl = audioUrl; }); } catch (headerError) { debugPrint( '⚠️ Error with headers, trying simple URL: $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 _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(); } Future _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 _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, audioUrl: ragResponse.audioUrl, )); _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), ), ], ), ); }, ), 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, ), ), ), ], ), const SizedBox(width: 12), const Expanded( child: DidvanText( 'دستیار هوشمند دیدوان', fontSize: 13, fontWeight: FontWeight.bold, color: Colors.white, ), ), Container( margin: const EdgeInsets.only(left: 6), decoration: const BoxDecoration( gradient: LinearGradient( begin: AlignmentGeometry.topLeft, end: AlignmentGeometry.bottomRight, colors: [ Color.fromARGB(255, 1, 35, 72), Color.fromARGB(255, 27, 60, 89), Color.fromARGB(255, 25, 93, 128), Color.fromARGB(255, 0, 126, 167), ], ), shape: BoxShape.circle, ), child: IconButton( icon: const Icon(Icons.headset_mic_rounded, color: Colors.white, size: 20), onPressed: () { Navigator.pop(context); showDialog( context: context, barrierDismissible: false, builder: (context) => const AiVoiceChatDialog(), ); }, splashRadius: 20, tooltip: 'گفتگوی صوتی', ), ), 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( 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 && // message.audioUrl != null && // message.audioUrl!.isNotEmpty) ...[ // const SizedBox(height: 8), // InkWell( // onTap: () => _toggleAudioPlayback(message.audioUrl!), // borderRadius: BorderRadius.circular(20), // child: Container( // padding: const EdgeInsets.symmetric( // horizontal: 12, vertical: 8), // decoration: BoxDecoration( // gradient: LinearGradient( // colors: [ // const Color(0xFF0066AA).withOpacity(0.1), // const Color(0xFF0088DD).withOpacity(0.08), // ], // ), // borderRadius: BorderRadius.circular(20), // border: Border.all( // color: const Color(0xFF0066AA).withOpacity(0.3), // width: 1, // ), // ), // child: Row( // mainAxisSize: MainAxisSize.min, // children: [ // Container( // padding: const EdgeInsets.all(4), // decoration: const BoxDecoration( // gradient: LinearGradient( // colors: [ // Color(0xFF0066AA), // Color(0xFF0088DD), // ], // ), // shape: BoxShape.circle, // ), // child: Icon( // (_isPlaying && // _currentPlayingUrl == message.audioUrl) // ? Icons.pause_rounded // : Icons.play_arrow_rounded, // size: 16, // color: Colors.white, // ), // ), // const SizedBox(width: 8), // DidvanText( // (_isPlaying && // _currentPlayingUrl == message.audioUrl) // ? 'در حال پخش...' // : 'پخش صوتی پاسخ', // fontSize: 12, // 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( 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: [ GestureDetector( onTap: _isLoading || _isRecording ? null : _sendMessage, child: AnimatedContainer( duration: const Duration(milliseconds: 200), width: 44, height: 55, 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)) .withOpacity(0.35), blurRadius: 12, offset: const Offset(0, 3), ), ], ), child: _isLoading ? const Padding( padding: EdgeInsets.all(11), child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Padding( padding: const EdgeInsets.all(8.0), child: SvgPicture.asset( 'lib/assets/icons/send.svg', color: Colors.white, height: 5, ), ), ), ), const SizedBox(width: 10), 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, ), ), ), ), ), // 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)) // .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, // ), // ), // ), ], ), ); } 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 sources; final String? audioUrl; ChatMessage({ required this.text, required this.isUser, required this.timestamp, this.sources = const [], this.audioUrl, }); }