1069 lines
35 KiB
Dart
1069 lines
35 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/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<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();
|
|
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<void> _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<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();
|
|
}
|
|
|
|
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,
|
|
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: GestureDetector(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(6.0),
|
|
child: SvgPicture.asset('lib/assets/icons/voice-square.svg',
|
|
color: Colors.white, height: 35),
|
|
),
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => const AiVoiceChatDialog(),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
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 &&
|
|
// 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<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: [
|
|
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<Color>(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<int> sources;
|
|
final String? audioUrl;
|
|
|
|
ChatMessage({
|
|
required this.text,
|
|
required this.isUser,
|
|
required this.timestamp,
|
|
this.sources = const [],
|
|
this.audioUrl,
|
|
});
|
|
}
|