didvan-app/lib/views/widgets/ai_chat_dialog.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,
});
}