Files
ai_english/client/lib/features/speaking/screens/ai_conversation_screen.dart
2025-11-17 14:09:17 +08:00

604 lines
22 KiB
Dart

import 'package:flutter/material.dart';
import '../models/ai_tutor.dart';
import '../models/conversation.dart';
/// AI对话页面
class AIConversationScreen extends StatefulWidget {
final AITutor tutor;
const AIConversationScreen({
super.key,
required this.tutor,
});
@override
State<AIConversationScreen> createState() => _AIConversationScreenState();
}
class _AIConversationScreenState extends State<AIConversationScreen> {
final TextEditingController _messageController = TextEditingController();
final ScrollController _scrollController = ScrollController();
final List<ConversationMessage> _messages = [];
bool _isTyping = false;
bool _isRecording = false;
@override
void initState() {
super.initState();
_initializeConversation();
}
void _initializeConversation() {
// 添加导师的欢迎消息
final welcomeMessage = ConversationMessage(
id: 'welcome_${DateTime.now().millisecondsSinceEpoch}',
content: _getWelcomeMessage(),
type: MessageType.ai,
timestamp: DateTime.now(),
);
setState(() {
_messages.add(welcomeMessage);
});
}
String _getWelcomeMessage() {
switch (widget.tutor.type) {
case TutorType.business:
return '${widget.tutor.introduction}\n\n让我们开始商务英语对话练习吧!你可以告诉我你的工作背景,或者我们可以模拟一个商务场景。';
case TutorType.daily:
return '${widget.tutor.introduction}\n\n今天想聊什么呢?我们可以谈论天气、兴趣爱好,或者你今天做了什么有趣的事情!';
case TutorType.travel:
return '${widget.tutor.introduction}\n\n准备好开始我们的旅行英语之旅了吗?告诉我你想去哪里旅行,或者我们可以模拟在机场、酒店的场景!';
case TutorType.academic:
return '${widget.tutor.introduction}\n\n欢迎来到学术英语课堂!我们可以讨论你的研究领域,或者练习学术演讲技巧。';
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
backgroundColor: widget.tutor.type.color,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
title: Row(
children: [
Text(
widget.tutor.avatar,
style: const TextStyle(fontSize: 24),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.tutor.name,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
widget.tutor.type.displayName,
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
),
),
],
),
),
],
),
actions: [
IconButton(
icon: const Icon(Icons.info_outline, color: Colors.white),
onPressed: _showTutorInfo,
),
],
),
body: Column(
children: [
Expanded(
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16),
itemCount: _messages.length + (_isTyping ? 1 : 0),
itemBuilder: (context, index) {
if (index == _messages.length && _isTyping) {
return _buildTypingIndicator();
}
return _buildMessageBubble(_messages[index]);
},
),
),
_buildInputArea(),
],
),
);
}
Widget _buildMessageBubble(ConversationMessage message) {
final isUser = message.type == MessageType.user;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isUser) ...[
CircleAvatar(
radius: 16,
backgroundColor: widget.tutor.type.color.withOpacity(0.1),
child: Text(
widget.tutor.avatar,
style: const TextStyle(fontSize: 16),
),
),
const SizedBox(width: 8),
],
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isUser ? widget.tutor.type.color : Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
message.content,
style: TextStyle(
color: isUser ? Colors.white : Colors.black87,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
_formatTime(message.timestamp),
style: TextStyle(
color: isUser ? Colors.white70 : Colors.grey,
fontSize: 10,
),
),
],
),
),
),
if (isUser) ...[
const SizedBox(width: 8),
CircleAvatar(
radius: 16,
backgroundColor: Colors.blue.withOpacity(0.1),
child: const Icon(
Icons.person,
size: 16,
color: Colors.blue,
),
),
],
],
),
);
}
Widget _buildTypingIndicator() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
CircleAvatar(
radius: 16,
backgroundColor: widget.tutor.type.color.withOpacity(0.1),
child: Text(
widget.tutor.avatar,
style: const TextStyle(fontSize: 16),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildDot(0),
const SizedBox(width: 4),
_buildDot(1),
const SizedBox(width: 4),
_buildDot(2),
],
),
),
],
),
);
}
Widget _buildDot(int index) {
return AnimatedContainer(
duration: Duration(milliseconds: 600 + (index * 200)),
width: 6,
height: 6,
decoration: BoxDecoration(
color: widget.tutor.type.color.withOpacity(0.6),
shape: BoxShape.circle,
),
);
}
Widget _buildInputArea() {
return Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 5,
offset: Offset(0, -2),
),
],
),
child: Row(
children: [
GestureDetector(
onTapDown: (_) => _startRecording(),
onTapUp: (_) => _stopRecording(),
onTapCancel: () => _stopRecording(),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _isRecording ? Colors.red : widget.tutor.type.color,
shape: BoxShape.circle,
),
child: Icon(
_isRecording ? Icons.stop : Icons.mic,
color: Colors.white,
size: 20,
),
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _messageController,
decoration: InputDecoration(
hintText: '输入消息...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey[100],
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onSubmitted: (_) => _sendMessage(),
),
),
const SizedBox(width: 12),
GestureDetector(
onTap: _sendMessage,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: widget.tutor.type.color,
shape: BoxShape.circle,
),
child: const Icon(
Icons.send,
color: Colors.white,
size: 20,
),
),
),
],
),
);
}
void _sendMessage() {
final text = _messageController.text.trim();
if (text.isEmpty) return;
// 添加用户消息
final userMessage = ConversationMessage(
id: 'user_${DateTime.now().millisecondsSinceEpoch}',
content: text,
type: MessageType.user,
timestamp: DateTime.now(),
);
setState(() {
_messages.add(userMessage);
_isTyping = true;
});
_messageController.clear();
_scrollToBottom();
// 生成AI回复
_generateAIResponse(text);
}
void _generateAIResponse(String userMessage) {
// 模拟AI思考时间
Future.delayed(const Duration(milliseconds: 1500), () {
final aiResponse = _getAIResponse(userMessage);
final aiMessage = ConversationMessage(
id: 'ai_${DateTime.now().millisecondsSinceEpoch}',
content: aiResponse,
type: MessageType.ai,
timestamp: DateTime.now(),
);
setState(() {
_isTyping = false;
_messages.add(aiMessage);
});
_scrollToBottom();
});
}
String _getAIResponse(String userMessage) {
final message = userMessage.toLowerCase();
// 根据导师类型和用户消息生成相应回复
switch (widget.tutor.type) {
case TutorType.business:
return _getBusinessResponse(message);
case TutorType.daily:
return _getDailyResponse(message);
case TutorType.travel:
return _getTravelResponse(message);
case TutorType.academic:
return _getAcademicResponse(message);
}
}
String _getBusinessResponse(String message) {
if (message.contains('meeting') || message.contains('会议')) {
return "Great! Let's discuss meeting preparation. What type of meeting are you attending? Is it a client presentation, team meeting, or board meeting?";
} else if (message.contains('presentation') || message.contains('演讲')) {
return "Presentations are crucial in business. What's your presentation topic? I can help you structure your content and practice key phrases.";
} else if (message.contains('email') || message.contains('邮件')) {
return "Business emails require professional tone. Are you writing to a client, colleague, or supervisor? What's the main purpose of your email?";
} else if (message.contains('hello') || message.contains('hi') || message.contains('你好')) {
return "Hello! I'm your business English tutor. I can help you with presentations, meetings, negotiations, and professional communication. What would you like to practice today?";
} else {
return "That's an interesting point. In business context, we should consider the professional implications. Could you elaborate on your specific business scenario?";
}
}
String _getDailyResponse(String message) {
if (message.contains('weather') || message.contains('天气')) {
return "The weather is a great conversation starter! How's the weather where you are? You can say 'It's sunny/rainy/cloudy today' or 'What a beautiful day!'";
} else if (message.contains('food') || message.contains('') || message.contains('')) {
return "Food is always a fun topic! What's your favorite cuisine? You can practice ordering food or describing flavors. Try saying 'I'd like to order...' or 'This tastes delicious!'";
} else if (message.contains('hobby') || message.contains('爱好')) {
return "Hobbies are personal and interesting! What do you enjoy doing in your free time? You can say 'I enjoy...' or 'My hobby is...' or 'In my spare time, I like to...'";
} else if (message.contains('hello') || message.contains('hi') || message.contains('你好')) {
return "Hi there! I'm here to help you with everyday English conversations. We can talk about weather, food, hobbies, shopping, or any daily activities. What interests you?";
} else {
return "That's interesting! In daily conversations, we often share personal experiences. Can you tell me more about that? It's great practice for natural English!";
}
}
String _getTravelResponse(String message) {
if (message.contains('airport') || message.contains('flight') || message.contains('机场')) {
return "Airport conversations are essential for travelers! Are you checking in, going through security, or asking for directions? Try phrases like 'Where is gate B12?' or 'Is this flight delayed?'";
} else if (message.contains('hotel') || message.contains('酒店')) {
return "Hotel interactions are important! Are you checking in, asking about amenities, or reporting an issue? Practice saying 'I have a reservation under...' or 'Could you help me with...'";
} else if (message.contains('restaurant') || message.contains('餐厅')) {
return "Dining out while traveling is fun! Are you making a reservation, ordering food, or asking about local specialties? Try 'Table for two, please' or 'What do you recommend?'";
} else if (message.contains('direction') || message.contains('')) {
return "Getting directions is crucial when traveling! Practice asking 'How do I get to...?' or 'Is it walking distance?' You can also say 'Could you show me on the map?'";
} else if (message.contains('hello') || message.contains('hi') || message.contains('你好')) {
return "Welcome, fellow traveler! I'm your travel English companion. I can help you with airport conversations, hotel bookings, restaurant orders, and asking for directions. Where shall we start?";
} else {
return "That sounds like a great travel experience! When traveling, it's important to communicate clearly. Can you describe the situation in more detail? I'll help you with the right phrases!";
}
}
String _getAcademicResponse(String message) {
if (message.contains('research') || message.contains('研究')) {
return "Research is fundamental in academics! What's your research area? We can practice presenting findings, discussing methodology, or explaining complex concepts clearly.";
} else if (message.contains('presentation') || message.contains('论文')) {
return "Academic presentations require clear structure and precise language. Are you presenting research results, defending a thesis, or giving a conference talk? Let's work on your key points!";
} else if (message.contains('discussion') || message.contains('讨论')) {
return "Academic discussions involve critical thinking and evidence-based arguments. What topic are you discussing? Practice phrases like 'According to the research...' or 'The evidence suggests...'";
} else if (message.contains('writing') || message.contains('写作')) {
return "Academic writing has specific conventions. Are you working on an essay, research paper, or thesis? I can help with structure, citations, and formal language.";
} else if (message.contains('hello') || message.contains('hi') || message.contains('你好')) {
return "Greetings! I'm your academic English tutor. I specialize in research discussions, academic presentations, scholarly writing, and conference communications. What academic skill would you like to develop?";
} else {
return "That's a thoughtful academic point. In scholarly discourse, we need to support our arguments with evidence. Could you provide more context or examples to strengthen your position?";
}
}
void _startRecording() {
setState(() {
_isRecording = true;
});
// TODO: 实现语音录制功能
}
void _stopRecording() {
setState(() {
_isRecording = false;
});
// TODO: 处理录制的语音
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
void _showTutorInfo() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
height: MediaQuery.of(context).size.height * 0.7,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
Container(
width: 40,
height: 4,
margin: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
widget.tutor.avatar,
style: const TextStyle(fontSize: 40),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.tutor.name,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
widget.tutor.type.displayName,
style: TextStyle(
fontSize: 14,
color: widget.tutor.type.color,
),
),
],
),
),
],
),
const SizedBox(height: 20),
Text(
'个性特点',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: widget.tutor.type.color,
),
),
const SizedBox(height: 8),
Text(
widget.tutor.personality,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 20),
Text(
'专业领域',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: widget.tutor.type.color,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: widget.tutor.specialties.map((specialty) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: widget.tutor.type.color.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: widget.tutor.type.color.withOpacity(0.3),
),
),
child: Text(
specialty,
style: TextStyle(
fontSize: 12,
color: widget.tutor.type.color,
),
),
);
}).toList(),
),
],
),
),
),
],
),
),
);
}
String _formatTime(DateTime time) {
return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
}
@override
void dispose() {
_messageController.dispose();
_scrollController.dispose();
super.dispose();
}
}