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

816 lines
24 KiB
Dart

// 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/views/widgets/didvan/text.dart';
import 'package:flutter/material.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';
import 'dart:math' as math;
class AiVoiceChatDialog extends StatefulWidget {
const AiVoiceChatDialog({super.key});
@override
State<AiVoiceChatDialog> createState() => _AiVoiceChatDialogState();
}
class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
with TickerProviderStateMixin {
bool _isRecording = false;
bool _isPreparing = false;
bool _isProcessing = false;
bool _isAiSpeaking = false;
String _statusText = 'برای شروع مکالمه، دکمه میکروفون را نگه دارید';
late AnimationController _waveController;
late AnimationController _pulseController;
late AnimationController _glowController;
late AnimationController _particleController;
late AnimationController _preparingController;
final AudioRecorder _audioRecorder = AudioRecorder();
final AudioPlayer _audioPlayer = AudioPlayer();
String? _recordingPath;
final List<double> _audioWaveHeights = List.generate(40, (_) => 0.3);
int _currentWaveIndex = 0;
final List<String> _thinkingMessages = [
'در حال فکر کردن...',
'در حال بررسی اطلاعات...',
'در حال تحلیل سوال شما...',
'در حال جستجو در دانش...',
'در حال پردازش درخواست...',
'در حال یافتن بهترین پاسخ...',
'در حال بررسی جزئیات...',
'در حال تحلیل داده‌های مرتبط...',
'در حال مقایسه‌ی نتایج ممکن...',
'در حال جمع‌آوری اطلاعات موردنیاز...',
'در حال تشخیص الگوها...'
];
final List<String> _analyzingMessages = [
'در حال تجزیه و تحلیل...',
'در حال پردازش داده‌ها...',
'در حال بررسی محتوا...',
'در حال ترکیب اطلاعات...',
'در حال درک منظور شما...',
'در حال آماده‌سازی پاسخ...',
'در حال محاسبه‌ی بهترین گزینه...',
'در حال درک مفهوم پرسش شما...',
'در حال به‌روزرسانی اطلاعات...',
'در حال استخراج پاسخ مناسب...',
'در حال هماهنگ‌سازی با پایگاه دانش...'
];
int _currentThinkingIndex = 0;
int _currentAnalyzingIndex = 0;
@override
void initState() {
super.initState();
_waveController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 100),
)..addListener(() {
if (_isRecording || _isAiSpeaking) {
setState(() {
_currentWaveIndex =
(_currentWaveIndex + 1) % _audioWaveHeights.length;
for (int i = 0; i < _audioWaveHeights.length; i++) {
_audioWaveHeights[i] = 0.3 + (math.Random().nextDouble() * 0.7);
}
});
}
});
_pulseController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
)..repeat(reverse: true);
_glowController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
)..repeat(reverse: true);
_particleController = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
)..repeat();
_preparingController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
)..repeat(reverse: true);
_audioPlayer.playerStateStream.listen((state) {
if (mounted) {
setState(() {
_isAiSpeaking = state.playing;
if (state.processingState == ProcessingState.completed) {
_isAiSpeaking = false;
_statusText = 'برای ادامه مکالمه، دکمه میکروفون را نگه دارید';
_waveController.stop();
} else if (state.playing) {
_statusText = 'دستیار در حال پاسخ دادن است...';
_waveController.repeat();
}
});
}
});
}
@override
void dispose() {
_waveController.dispose();
_pulseController.dispose();
_glowController.dispose();
_particleController.dispose();
_preparingController.dispose();
_audioRecorder.dispose();
_audioPlayer.dispose();
super.dispose();
}
Future<void> _startRecording() async {
try {
if (await _audioRecorder.hasPermission()) {
if (_isAiSpeaking) {
await _audioPlayer.stop();
setState(() {
_isAiSpeaking = false;
});
}
setState(() {
_isPreparing = true;
_statusText = '⏳ در حال آماده سازی...';
});
_preparingController.repeat(reverse: 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(seconds: 2));
if (mounted && _isPreparing) {
setState(() {
_isPreparing = false;
_isRecording = true;
_statusText = '🎙️ در حال گوش دادن...';
});
_preparingController.stop();
_waveController.repeat();
}
}
} catch (e) {
setState(() {
_isPreparing = false;
_isRecording = false;
_statusText = 'خطا در شروع ضبط صدا';
});
_preparingController.stop();
_waveController.stop();
debugPrint('Error starting recording: $e');
}
}
Future<void> _stopRecording() async {
if (!_isRecording) return;
try {
setState(() {
_isRecording = false;
_isProcessing = true;
_currentThinkingIndex = 0;
_statusText = _thinkingMessages[_currentThinkingIndex];
});
_waveController.stop();
_startThinkingMessageRotation();
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return;
final path = await _audioRecorder.stop();
if (path != null) {
final response = await AiVoiceService.uploadVoice(path);
if (response.isSuccess && response.text.isNotEmpty) {
setState(() {
_currentAnalyzingIndex = 0;
_statusText = _analyzingMessages[_currentAnalyzingIndex];
});
_startAnalyzingMessageRotation();
final ragResponse = await AiRagService.sendMessage(response.text);
if (ragResponse.audioUrl != null &&
ragResponse.audioUrl!.isNotEmpty) {
setState(() {
_statusText = '🔊 دستیار در حال پاسخ دادن است...';
_isProcessing = false;
_isAiSpeaking = true;
});
await _audioPlayer.setUrl(ragResponse.audioUrl!);
await _audioPlayer.play();
_waveController.repeat();
} else {
setState(() {
_statusText = 'خطا در پردازش';
_isProcessing = false;
});
}
} else {
setState(() {
_statusText = 'خطا در پردازش پیام صوتی';
_isProcessing = false;
});
}
try {
await File(path).delete();
} catch (e) {
debugPrint('Error deleting temp file: $e');
}
} else {
setState(() {
_isProcessing = false;
_statusText = 'خطا در ضبط صدا';
});
}
} catch (e) {
setState(() {
_isRecording = false;
_isProcessing = false;
_statusText = 'خطا در پردازش: ${e.toString()}';
});
_waveController.stop();
debugPrint('Error stopping recording: $e');
}
}
void _startThinkingMessageRotation() {
Future.delayed(const Duration(seconds: 10), () {
if (mounted && _isProcessing && !_isAiSpeaking) {
setState(() {
_currentThinkingIndex =
(_currentThinkingIndex + 1) % _thinkingMessages.length;
_statusText = _thinkingMessages[_currentThinkingIndex];
});
_startThinkingMessageRotation();
}
});
}
void _startAnalyzingMessageRotation() {
Future.delayed(const Duration(seconds: 10), () {
if (mounted && _isProcessing && !_isAiSpeaking) {
setState(() {
_currentAnalyzingIndex =
(_currentAnalyzingIndex + 1) % _analyzingMessages.length;
_statusText = _analyzingMessages[_currentAnalyzingIndex];
});
_startAnalyzingMessageRotation();
}
});
}
Color _getMainColor() {
if (_isPreparing) return const Color(0xFFFFA500);
if (_isRecording) return const Color.fromARGB(255, 178, 4, 54);
if (_isAiSpeaking) return const Color(0xFF00AAFF);
if (_isProcessing) return const Color(0xFFFFAA00);
return const Color(0xFF6B7280);
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 700),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF0A0E27).withOpacity(0.95),
const Color(0xFF1A1F3A).withOpacity(0.95),
],
),
borderRadius: BorderRadius.circular(32),
border: Border.all(
color: Colors.white.withOpacity(0.1),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: _getMainColor().withOpacity(0.3),
blurRadius: 40,
spreadRadius: 0,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(32),
child: Stack(
children: [
_buildAnimatedBackground(),
Column(
children: [
_buildHeader(),
Expanded(
child: _buildMainContent(),
),
_buildControls(),
],
),
],
),
),
),
),
);
}
Widget _buildAnimatedBackground() {
return AnimatedBuilder(
animation: _particleController,
builder: (context, child) {
return CustomPaint(
painter: ParticlePainter(
animation: _particleController,
color: _getMainColor(),
),
size: Size.infinite,
);
},
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white.withOpacity(0.05),
Colors.transparent,
],
),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [_getMainColor(), _getMainColor().withOpacity(0.6)],
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: _getMainColor().withOpacity(0.5),
blurRadius: 20,
spreadRadius: 2,
),
],
),
child: Icon(
_isRecording
? Icons.mic_rounded
: _isAiSpeaking
? Icons.volume_up_rounded
: Icons.headset_mic_rounded,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DidvanText(
'گفتگوی صوتی',
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
SizedBox(height: 4),
DidvanText(
'دستیار هوشمند دیدوان',
fontSize: 12,
color: Colors.white60,
),
],
),
),
IconButton(
icon: const Icon(Icons.close_rounded, color: Colors.white70),
onPressed: () => Navigator.pop(context),
),
],
),
);
}
Widget _buildMainContent() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildVisualization(),
const SizedBox(height: 40),
_buildStatusText(),
],
),
);
}
Widget _buildVisualization() {
return AnimatedBuilder(
animation: Listenable.merge(
[_pulseController, _glowController, _preparingController]),
builder: (context, child) {
final pulseValue = _pulseController.value;
final glowValue = _glowController.value;
final preparingValue = _preparingController.value;
return Stack(
alignment: Alignment.center,
children: [
if (_isPreparing) ...[
for (int i = 0; i < 4; i++)
Transform.scale(
scale: 1.0 + (preparingValue * 0.3) + (i * 0.15),
child: Container(
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFFFFA500)
.withOpacity(0.4 - (i * 0.1)),
width: 3,
),
),
),
),
],
if (!_isPreparing)
for (int i = 0; i < 3; i++)
Container(
width: 280 + (i * 40) + (pulseValue * 20),
height: 280 + (i * 40) + (pulseValue * 20),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: _getMainColor().withOpacity(0.1 - (i * 0.03)),
width: 2,
),
),
),
Container(
width: 260 + (glowValue * 20),
height: 260 + (glowValue * 20),
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
_getMainColor().withOpacity(0.3),
_getMainColor().withOpacity(0.1),
Colors.transparent,
],
),
),
),
if (_isPreparing)
SizedBox(
width: 240,
height: 240,
child: CustomPaint(
painter: PreparingSpinnerPainter(
animation: preparingValue,
color: const Color(0xFFFFA500),
),
),
)
else
SizedBox(
width: 240,
height: 240,
child: CustomPaint(
painter: WaveVisualizerPainter(
waveHeights: _audioWaveHeights,
color: _getMainColor(),
isActive: _isRecording || _isAiSpeaking,
),
),
),
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
_getMainColor(),
_getMainColor().withOpacity(0.7),
],
),
boxShadow: [
BoxShadow(
color: _getMainColor().withOpacity(0.6),
blurRadius: 30,
spreadRadius: 5,
),
],
),
child: Icon(
_isPreparing
? Icons.settings_rounded
: _isRecording
? Icons.mic_rounded
: _isAiSpeaking
? Icons.graphic_eq_rounded
: _isProcessing
? Icons.hourglass_empty_rounded
: Icons.headphones_rounded,
color: Colors.white,
size: 50,
),
),
],
);
},
);
}
Widget _buildStatusText() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 32),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: _getMainColor().withOpacity(0.3),
width: 1,
),
),
child: DidvanText(
_statusText,
fontSize: 14,
color: Colors.white,
textAlign: TextAlign.center,
),
);
}
Widget _buildControls() {
return Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_isAiSpeaking)
const Padding(
padding: EdgeInsets.only(bottom: 16),
child: DidvanText(
'',
fontSize: 12,
color: Colors.white70,
textAlign: TextAlign.center,
),
),
GestureDetector(
onLongPressStart: (_) {
if (!_isProcessing && !_isRecording && !_isPreparing) {
_startRecording();
}
},
onLongPressEnd: (_) {
if (_isRecording) {
_stopRecording();
}
},
child: AnimatedBuilder(
animation: _pulseController,
builder: (context, child) {
return Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: _isPreparing
? [const Color(0xFFFFA500), const Color(0xFFFFCC00)]
: _isRecording
? [
const Color.fromARGB(255, 178, 4, 54),
const Color.fromARGB(255, 255, 200, 215)
]
: _isProcessing
? [Colors.grey.shade600, Colors.grey.shade700]
: [
const Color(0xFF0066AA),
const Color(0xFF00AAFF)
],
),
boxShadow: [
BoxShadow(
color: _getMainColor().withOpacity(0.5),
blurRadius: 20 + (_pulseController.value * 10),
spreadRadius: 5 + (_pulseController.value * 5),
),
],
),
child: Icon(
_isPreparing || _isRecording
? Icons.stop_rounded
: Icons.mic_rounded,
color: Colors.white,
size: 36,
),
);
},
),
),
],
),
);
}
}
class WaveVisualizerPainter extends CustomPainter {
final List<double> waveHeights;
final Color color;
final bool isActive;
WaveVisualizerPainter({
required this.waveHeights,
required this.color,
required this.isActive,
});
@override
void paint(Canvas canvas, Size size) {
if (!isActive) return;
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2;
final barCount = waveHeights.length;
final angleStep = (2 * math.pi) / barCount;
for (int i = 0; i < barCount; i++) {
final angle = i * angleStep;
final height = waveHeights[i] * 40;
final startX = center.dx + (radius - 10) * math.cos(angle);
final startY = center.dy + (radius - 10) * math.sin(angle);
final endX = center.dx + (radius - 10 + height) * math.cos(angle);
final endY = center.dy + (radius - 10 + height) * math.sin(angle);
paint.strokeWidth = 3;
paint.strokeCap = StrokeCap.round;
paint.color = color.withOpacity(0.6 + (waveHeights[i] * 0.4));
canvas.drawLine(
Offset(startX, startY),
Offset(endX, endY),
paint,
);
}
}
@override
bool shouldRepaint(covariant WaveVisualizerPainter oldDelegate) {
return true;
}
}
class ParticlePainter extends CustomPainter {
final Animation<double> animation;
final Color color;
ParticlePainter({required this.animation, required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color.withOpacity(0.1)
..style = PaintingStyle.fill;
final random = math.Random(42);
for (int i = 0; i < 30; i++) {
final x = random.nextDouble() * size.width;
final baseY = random.nextDouble() * size.height;
final y = baseY + (animation.value * 100) % size.height;
final radius = 1 + random.nextDouble() * 2;
canvas.drawCircle(Offset(x, y), radius, paint);
}
}
@override
bool shouldRepaint(covariant ParticlePainter oldDelegate) {
return true;
}
}
class PreparingSpinnerPainter extends CustomPainter {
final double animation;
final Color color;
PreparingSpinnerPainter({required this.animation, required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 4
..strokeCap = StrokeCap.round;
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2 - 20;
const dotCount = 12;
const angleStep = (2 * math.pi) / dotCount;
for (int i = 0; i < dotCount; i++) {
final angle = (i * angleStep) + (animation * 2 * math.pi);
final opacity = (1.0 - (i / dotCount)) * 0.8;
final x = center.dx + radius * math.cos(angle);
final y = center.dy + radius * math.sin(angle);
paint.color = color.withOpacity(opacity);
canvas.drawCircle(
Offset(x, y),
3 + (animation * 2),
paint..style = PaintingStyle.fill,
);
}
for (int i = 0; i < 3; i++) {
final arcRadius = radius - (i * 25);
final startAngle = (animation * 2 * math.pi) + (i * math.pi / 3);
final sweepAngle = math.pi / 2 + (animation * math.pi / 4);
paint
..style = PaintingStyle.stroke
..strokeWidth = 3.0 - i
..color = color.withOpacity(0.5 - (i * 0.1));
canvas.drawArc(
Rect.fromCircle(center: center, radius: arcRadius),
startAngle,
sweepAngle,
false,
paint,
);
}
}
@override
bool shouldRepaint(covariant PreparingSpinnerPainter oldDelegate) {
return true;
}
}