|
|
|
@ -183,10 +183,13 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver { |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
body: Column( |
|
|
|
body: Stack( |
|
|
|
children: [ |
|
|
|
// 聊天内容 |
|
|
|
Expanded( |
|
|
|
// 主要聊天界面 |
|
|
|
Column( |
|
|
|
children: [ |
|
|
|
// 聊天内容 |
|
|
|
Expanded( |
|
|
|
child: Obx( |
|
|
|
() => Align( |
|
|
|
alignment: Alignment.topCenter, |
|
|
|
@ -407,12 +410,12 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver { |
|
|
|
textInputAction: TextInputAction |
|
|
|
.newline, // 支持回车换行 |
|
|
|
onTap: () { |
|
|
|
// 当点击输入框时,如果图片选项已展开,则收缩 |
|
|
|
// 当点击输入框时,如果'+'选项已展开,则收缩 |
|
|
|
if (controller |
|
|
|
.isImageOptionsExpanded |
|
|
|
.isPlusOptionsExpanded |
|
|
|
.value) { |
|
|
|
controller |
|
|
|
.collapseImageOptions(); |
|
|
|
.collapsePlusOptions(); |
|
|
|
} |
|
|
|
}, |
|
|
|
style: TextStyle( |
|
|
|
@ -607,10 +610,10 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver { |
|
|
|
// 显示'+'选择按钮 |
|
|
|
return Obx(() { |
|
|
|
final isExpanded = |
|
|
|
controller.isImageOptionsExpanded.value; |
|
|
|
controller.isPlusOptionsExpanded.value; |
|
|
|
return GestureDetector( |
|
|
|
onTap: () { |
|
|
|
controller.toggleImageOptions(); |
|
|
|
controller.togglePlusOptions(); |
|
|
|
}, |
|
|
|
child: Container( |
|
|
|
width: 44, |
|
|
|
@ -640,11 +643,11 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver { |
|
|
|
// 展开的'+'选择选项 |
|
|
|
Obx(() { |
|
|
|
final isExpanded = |
|
|
|
controller.isImageOptionsExpanded.value; |
|
|
|
controller.isPlusOptionsExpanded.value; |
|
|
|
return AnimatedContainer( |
|
|
|
duration: const Duration(milliseconds: 300), |
|
|
|
curve: Curves.easeInOut, |
|
|
|
height: isExpanded ? 100.h : 0, // 调整高度以适应按钮尺寸 |
|
|
|
height: isExpanded ? 120.h : 0, // 调整高度以适应按钮尺寸 |
|
|
|
child: isExpanded |
|
|
|
? Container( |
|
|
|
margin: EdgeInsets.only(top: 8.h), |
|
|
|
@ -658,7 +661,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver { |
|
|
|
icon: Icons.camera_alt, |
|
|
|
label: '拍照', |
|
|
|
onTap: () async { |
|
|
|
//controller.collapseImageOptions(); |
|
|
|
//controller.collapsePlusOptions(); |
|
|
|
await controller.takePhoto(); |
|
|
|
}, |
|
|
|
isDarkMode: isDarkMode, |
|
|
|
@ -669,7 +672,7 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver { |
|
|
|
icon: Icons.photo_library, |
|
|
|
label: '相册', |
|
|
|
onTap: () async { |
|
|
|
//controller.collapseImageOptions(); |
|
|
|
//controller.collapsePlusOptions(); |
|
|
|
await controller.pickImage(); |
|
|
|
}, |
|
|
|
isDarkMode: isDarkMode, |
|
|
|
@ -680,19 +683,19 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver { |
|
|
|
// icon: Icons.folder, |
|
|
|
// label: '文件', |
|
|
|
// onTap: () { |
|
|
|
// //controller.collapseImageOptions(); |
|
|
|
// //controller.collapsePlusOptions(); |
|
|
|
// // TODO: 实现文件选择功能 |
|
|
|
// }, |
|
|
|
// isDarkMode: isDarkMode, |
|
|
|
// ), |
|
|
|
// 通话按钮(预留位置) |
|
|
|
// 通话按钮 |
|
|
|
_buildPlusOptionButton( |
|
|
|
context: context, |
|
|
|
icon: Icons.phone, |
|
|
|
label: '打电话', |
|
|
|
onTap: () { |
|
|
|
//controller.collapseImageOptions(); |
|
|
|
// TODO: 实现通话功能 |
|
|
|
// 启动打电话模式 |
|
|
|
controller.startPhoneCall(); |
|
|
|
}, |
|
|
|
isDarkMode: isDarkMode, |
|
|
|
), |
|
|
|
@ -707,6 +710,16 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver { |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
|
|
|
|
// 打电话模式覆盖层 |
|
|
|
Obx(() { |
|
|
|
if (controller.isPhoneCallMode.value) { |
|
|
|
return _buildPhoneCallOverlay(context, isDarkMode); |
|
|
|
} |
|
|
|
return const SizedBox.shrink(); |
|
|
|
}), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
@ -808,6 +821,239 @@ class _AgentViewState extends State<AgentView> with WidgetsBindingObserver { |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建打电话模式覆盖层 |
|
|
|
Widget _buildPhoneCallOverlay(BuildContext context, bool isDarkMode) { |
|
|
|
return Container( |
|
|
|
color: isDarkMode ? Colors.black : Colors.white, |
|
|
|
child: SafeArea( |
|
|
|
child: Column( |
|
|
|
children: [ |
|
|
|
// 顶部标题栏 |
|
|
|
_buildPhoneCallHeader(isDarkMode), |
|
|
|
|
|
|
|
// 中间音频可视化区域 |
|
|
|
Expanded( |
|
|
|
child: _buildPhoneCallVisualization(isDarkMode), |
|
|
|
), |
|
|
|
|
|
|
|
// 底部控制按钮 |
|
|
|
_buildPhoneCallControls(isDarkMode), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建打电话模式标题栏 |
|
|
|
Widget _buildPhoneCallHeader(bool isDarkMode) { |
|
|
|
return Container( |
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
// 返回按钮 |
|
|
|
GestureDetector( |
|
|
|
onTap: () => controller.endPhoneCall(), |
|
|
|
child: Container( |
|
|
|
width: 40, |
|
|
|
height: 40, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: isDarkMode ? Colors.grey[800] : Colors.grey[200], |
|
|
|
borderRadius: BorderRadius.circular(20), |
|
|
|
), |
|
|
|
child: Icon( |
|
|
|
Icons.arrow_back, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black, |
|
|
|
size: 20, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
|
|
|
|
const SizedBox(width: 16), |
|
|
|
|
|
|
|
// 标题 |
|
|
|
Expanded( |
|
|
|
child: Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
Text( |
|
|
|
'AI语音通话', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 18, |
|
|
|
fontWeight: FontWeight.w600, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black, |
|
|
|
), |
|
|
|
), |
|
|
|
Obx(() { |
|
|
|
final status = controller.isListening.value |
|
|
|
? '正在通话中...' |
|
|
|
: controller.isSpeaking.value |
|
|
|
? 'AI正在回复...' |
|
|
|
: '等待开始...'; |
|
|
|
return Text( |
|
|
|
status, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 14, |
|
|
|
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], |
|
|
|
), |
|
|
|
); |
|
|
|
}), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建打电话模式音频可视化 |
|
|
|
Widget _buildPhoneCallVisualization(bool isDarkMode) { |
|
|
|
return Center( |
|
|
|
child: Column( |
|
|
|
mainAxisAlignment: MainAxisAlignment.center, |
|
|
|
children: [ |
|
|
|
// 音频可视化圆圈 |
|
|
|
Obx(() { |
|
|
|
final isActive = controller.isListening.value || controller.isSpeaking.value; |
|
|
|
return AnimatedContainer( |
|
|
|
duration: const Duration(milliseconds: 300), |
|
|
|
width: isActive ? 200 : 160, |
|
|
|
height: isActive ? 200 : 160, |
|
|
|
decoration: BoxDecoration( |
|
|
|
shape: BoxShape.circle, |
|
|
|
color: isDarkMode |
|
|
|
? Colors.blue[900]?.withOpacity(0.3) |
|
|
|
: Colors.blue[100], |
|
|
|
border: Border.all( |
|
|
|
color: Colors.blue, |
|
|
|
width: isActive ? 3 : 2, |
|
|
|
), |
|
|
|
), |
|
|
|
child: Icon( |
|
|
|
Icons.phone, |
|
|
|
size: isActive ? 80 : 60, |
|
|
|
color: Colors.blue, |
|
|
|
), |
|
|
|
); |
|
|
|
}), |
|
|
|
|
|
|
|
const SizedBox(height: 32), |
|
|
|
|
|
|
|
// 状态文字 |
|
|
|
Obx(() { |
|
|
|
String statusText = '点击开始通话'; |
|
|
|
if (controller.isListening.value) { |
|
|
|
statusText = '正在听您说话...'; |
|
|
|
} else if (controller.isSpeaking.value) { |
|
|
|
statusText = 'AI正在回复...'; |
|
|
|
} else if (controller.isProcessing.value) { |
|
|
|
statusText = '正在处理...'; |
|
|
|
} |
|
|
|
|
|
|
|
return Text( |
|
|
|
statusText, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 16, |
|
|
|
color: isDarkMode ? Colors.grey[300] : Colors.grey[700], |
|
|
|
fontWeight: FontWeight.w500, |
|
|
|
), |
|
|
|
); |
|
|
|
}), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建打电话模式控制按钮 |
|
|
|
Widget _buildPhoneCallControls(bool isDarkMode) { |
|
|
|
return Container( |
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24), |
|
|
|
child: Row( |
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
|
|
|
children: [ |
|
|
|
// 静音按钮 |
|
|
|
Obx(() => _buildPhoneControlButton( |
|
|
|
icon: controller.isTtsEnabled.value ? Icons.volume_up : Icons.volume_off, |
|
|
|
onTap: () => controller.toggleTts(), |
|
|
|
isDarkMode: isDarkMode, |
|
|
|
isActive: controller.isTtsEnabled.value, |
|
|
|
)), |
|
|
|
|
|
|
|
// 主要通话按钮 |
|
|
|
Obx(() { |
|
|
|
final isActive = controller.isListening.value; |
|
|
|
return GestureDetector( |
|
|
|
onTap: () { |
|
|
|
if (isActive) { |
|
|
|
controller.stopVoiceInput(); |
|
|
|
} else { |
|
|
|
controller.startVoiceInput(); |
|
|
|
} |
|
|
|
}, |
|
|
|
child: Container( |
|
|
|
width: 80, |
|
|
|
height: 80, |
|
|
|
decoration: BoxDecoration( |
|
|
|
shape: BoxShape.circle, |
|
|
|
color: isActive ? Colors.red : Colors.green, |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: (isActive ? Colors.red : Colors.green).withOpacity(0.3), |
|
|
|
blurRadius: 12, |
|
|
|
spreadRadius: 2, |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: Icon( |
|
|
|
isActive ? Icons.call_end : Icons.call, |
|
|
|
color: Colors.white, |
|
|
|
size: 36, |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
}), |
|
|
|
|
|
|
|
// 结束通话按钮 |
|
|
|
_buildPhoneControlButton( |
|
|
|
icon: Icons.call_end, |
|
|
|
onTap: () => controller.endPhoneCall(), |
|
|
|
isDarkMode: isDarkMode, |
|
|
|
isActive: false, |
|
|
|
backgroundColor: Colors.red, |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建打电话控制按钮 |
|
|
|
Widget _buildPhoneControlButton({ |
|
|
|
required IconData icon, |
|
|
|
required VoidCallback onTap, |
|
|
|
required bool isDarkMode, |
|
|
|
required bool isActive, |
|
|
|
Color? backgroundColor, |
|
|
|
}) { |
|
|
|
return GestureDetector( |
|
|
|
onTap: onTap, |
|
|
|
child: Container( |
|
|
|
width: 56, |
|
|
|
height: 56, |
|
|
|
decoration: BoxDecoration( |
|
|
|
shape: BoxShape.circle, |
|
|
|
color: backgroundColor ?? (isDarkMode ? Colors.grey[800] : Colors.grey[200]), |
|
|
|
border: isActive ? Border.all(color: Colors.blue, width: 2) : null, |
|
|
|
), |
|
|
|
child: Icon( |
|
|
|
icon, |
|
|
|
color: backgroundColor != null |
|
|
|
? Colors.white |
|
|
|
: (isDarkMode ? Colors.white : Colors.black), |
|
|
|
size: 24, |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|