import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/core.dart'; import 'voice_mode_overlay.dart'; class InteractionScreen extends ConsumerStatefulWidget { final String characterId; const InteractionScreen({super.key, required this.characterId}); @override ConsumerState createState() => _InteractionScreenState(); } class _InteractionScreenState extends ConsumerState { CharacterModel? _character; List _messages = []; final TextEditingController _controller = TextEditingController(); final ScrollController _scrollController = ScrollController(); bool _isVoiceMode = false; bool _isLoading = false; bool _isTyping = false; String _typingContent = ''; @override void initState() { super.initState(); _loadCharacterAndMessages(); } @override void dispose() { _controller.dispose(); _scrollController.dispose(); super.dispose(); } Future _loadCharacterAndMessages() async { // 加载角色信息 final character = await CharacterRepository.getCharacterById(widget.characterId); if (character == null) { debugPrint('❌ 角色不存在: ${widget.characterId}'); if (mounted) context.pop(); return; } // 加载本地聊天记录 final session = await ChatStorageService.getSession(widget.characterId); setState(() { _character = character; _messages = session.messages; }); // 如果没有消息,添加问候语 if (_messages.isEmpty) { final greeting = ChatService.getGreeting(character); final greetingMessage = ChatMessage.assistant(greeting); setState(() { _messages = [greetingMessage]; }); await ChatStorageService.addMessage(widget.characterId, greetingMessage); } _scrollToBottom(); } void _scrollToBottom() { WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ); } }); } Future _sendMessage() async { if (_controller.text.trim().isEmpty || _character == null || _isLoading) return; final userText = _controller.text.trim(); _controller.clear(); // 添加用户消息 final userMessage = ChatMessage.user(userText); setState(() { _messages = [..._messages, userMessage]; _isLoading = true; _isTyping = true; _typingContent = ''; }); await ChatStorageService.addMessage(widget.characterId, userMessage); _scrollToBottom(); try { // 调用 AI final response = await ChatService.sendMessage( character: _character!, messages: _messages, userMessage: userText, onStream: (content) { if (mounted) { setState(() { _typingContent = content; }); _scrollToBottom(); } }, ); // 添加 AI 回复 final aiMessage = ChatMessage.assistant(response); setState(() { _messages = [..._messages, aiMessage]; _isTyping = false; _typingContent = ''; }); await ChatStorageService.addMessage(widget.characterId, aiMessage); _scrollToBottom(); } catch (e) { debugPrint('❌ 发送消息失败: $e'); // 显示错误消息 if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('发送失败: $e'), backgroundColor: Colors.red, ), ); setState(() { _isTyping = false; _typingContent = ''; }); } } finally { if (mounted) { setState(() { _isLoading = false; }); } } } @override Widget build(BuildContext context) { if (_character == null) { return const Scaffold( body: Center( child: CircularProgressIndicator(color: Color(0xFFA855F7)), ), ); } final avatarUrl = CharacterRepository.getAvatarUrl(_character!.avatarPath); return Stack( children: [ Scaffold( extendBodyBehindAppBar: true, appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, flexibleSpace: ClipRRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), child: Container(color: Colors.black.withOpacity(0.5)), ), ), leading: IconButton( icon: const Icon(LucideIcons.arrowLeft, color: Colors.white), onPressed: () => context.pop(), ), title: Row( children: [ CircleAvatar( backgroundImage: NetworkImage(avatarUrl), radius: 16, onBackgroundImageError: (_, __) {}, child: _character!.avatarPath == null ? const Icon(LucideIcons.user, size: 16, color: Colors.white54) : null, ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _character!.name, style: const TextStyle(fontSize: 16, color: Colors.white), overflow: TextOverflow.ellipsis, ), Row( children: [ Container( width: 6, height: 6, decoration: BoxDecoration( color: _getStatusColor(_character!.status), shape: BoxShape.circle, ), ), const SizedBox(width: 4), Text( _getStatusText(_character!.status), style: TextStyle(fontSize: 10, color: Colors.white.withOpacity(0.7)), ), ], ), ], ), ), ], ), actions: [ IconButton( icon: const Icon(LucideIcons.trash2, color: Colors.white54, size: 20), onPressed: _clearChat, tooltip: '清除聊天记录', ), IconButton( icon: const Icon(LucideIcons.moreVertical, color: Colors.white), onPressed: () {}, ), ], ), body: Container( decoration: const BoxDecoration( color: Color(0xFF2E1065), gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFF2E1065), Color(0xFF0F172A)], ), ), child: Column( children: [ Expanded( child: ListView.separated( controller: _scrollController, padding: const EdgeInsets.only(top: 120, bottom: 20, left: 16, right: 16), itemCount: _messages.length + (_isTyping ? 1 : 0), separatorBuilder: (_, __) => const SizedBox(height: 16), itemBuilder: (context, index) { // 如果是正在输入的消息 if (_isTyping && index == _messages.length) { return _buildTypingBubble(); } final msg = _messages[index]; return _buildMessageBubble(msg, avatarUrl); }, ), ), // Input Area _buildInputArea(), ], ), ), ), if (_isVoiceMode && _character != null) VoiceModeOverlay( character: _character!, onClose: () => setState(() => _isVoiceMode = false), ), ], ); } Widget _buildMessageBubble(ChatMessage msg, String avatarUrl) { final isMe = msg.isUser; return Row( mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!isMe) ...[ CircleAvatar( backgroundImage: NetworkImage(avatarUrl), radius: 16, onBackgroundImageError: (_, __) {}, ), const SizedBox(width: 8), ], Flexible( child: Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.7, ), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: isMe ? const Color(0xFFA855F7) : Colors.white.withOpacity(0.1), borderRadius: BorderRadius.only( topLeft: const Radius.circular(16), topRight: const Radius.circular(16), bottomLeft: isMe ? const Radius.circular(16) : const Radius.circular(2), bottomRight: isMe ? const Radius.circular(2) : const Radius.circular(16), ), border: Border.all(color: Colors.white.withOpacity(0.1)), ), child: Text( msg.content, style: const TextStyle(color: Colors.white, fontSize: 14, height: 1.4), ), ), ), if (isMe) const SizedBox(width: 8), ], ); } Widget _buildTypingBubble() { final avatarUrl = CharacterRepository.getAvatarUrl(_character!.avatarPath); return Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ CircleAvatar( backgroundImage: NetworkImage(avatarUrl), radius: 16, onBackgroundImageError: (_, __) {}, ), const SizedBox(width: 8), Flexible( child: Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.7, ), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.1), borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), bottomLeft: Radius.circular(2), bottomRight: Radius.circular(16), ), border: Border.all(color: Colors.white.withOpacity(0.1)), ), child: _typingContent.isEmpty ? Row( mainAxisSize: MainAxisSize.min, children: [ _buildDot(0), _buildDot(1), _buildDot(2), ], ) : Text( _typingContent, style: const TextStyle(color: Colors.white, fontSize: 14, height: 1.4), ), ), ), ], ); } Widget _buildDot(int index) { return TweenAnimationBuilder( tween: Tween(begin: 0.3, end: 1.0), duration: Duration(milliseconds: 600 + index * 200), curve: Curves.easeInOut, builder: (context, value, child) { return Container( margin: const EdgeInsets.symmetric(horizontal: 2), width: 8, height: 8, decoration: BoxDecoration( color: Colors.white.withOpacity(value), shape: BoxShape.circle, ), ); }, ); } Widget _buildInputArea() { return ClipRRect( child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), child: Container( padding: EdgeInsets.only( bottom: MediaQuery.of(context).padding.bottom + 10, top: 10, left: 16, right: 16, ), color: Colors.black.withOpacity(0.4), child: Row( children: [ GestureDetector( onTap: () => setState(() => _isVoiceMode = true), child: Container( width: 44, height: 44, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withOpacity(0.1), border: Border.all(color: Colors.white.withOpacity(0.1)), ), child: const Icon(LucideIcons.phone, color: Colors.white, size: 20), ), ), const SizedBox(width: 8), Expanded( child: TextField( controller: _controller, style: const TextStyle(color: Colors.white), enabled: !_isLoading, onSubmitted: (_) => _sendMessage(), decoration: InputDecoration( filled: true, fillColor: Colors.white.withOpacity(0.05), hintText: _isLoading ? 'AI 正在思考...' : '输入消息...', hintStyle: TextStyle(color: Colors.white.withOpacity(0.3)), border: OutlineInputBorder( borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), ), ), ), const SizedBox(width: 8), GestureDetector( onTap: _isLoading ? null : _sendMessage, child: Container( width: 44, height: 44, decoration: BoxDecoration( shape: BoxShape.circle, gradient: _isLoading ? null : const LinearGradient(colors: [Color(0xFFA855F7), Color(0xFFEC4899)]), color: _isLoading ? Colors.white.withOpacity(0.1) : null, ), child: _isLoading ? const Padding( padding: EdgeInsets.all(12), child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white54, ), ) : const Icon(LucideIcons.send, color: Colors.white, size: 20), ), ), ], ), ), ), ); } Future _clearChat() async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: const Color(0xFF2E1065), title: const Text('清除聊天记录', style: TextStyle(color: Colors.white)), content: const Text('确定要清除所有聊天记录吗?此操作不可恢复。', style: TextStyle(color: Colors.white70)), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('取消', style: TextStyle(color: Colors.white54)), ), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text('清除', style: TextStyle(color: Colors.red)), ), ], ), ); if (confirmed == true) { await ChatStorageService.clearSession(widget.characterId); final greeting = ChatService.getGreeting(_character!); final greetingMessage = ChatMessage.assistant(greeting); setState(() { _messages = [greetingMessage]; }); await ChatStorageService.addMessage(widget.characterId, greetingMessage); } } Color _getStatusColor(CharacterStatus status) { switch (status) { case CharacterStatus.online: return const Color(0xFF10B981); case CharacterStatus.busy: return const Color(0xFFF59E0B); case CharacterStatus.offline: return Colors.grey; } } String _getStatusText(CharacterStatus status) { switch (status) { case CharacterStatus.online: return '在线'; case CharacterStatus.busy: return '忙碌'; case CharacterStatus.offline: return '离线'; } } }