|
|
|
@ -10,42 +10,64 @@ class AgentView extends StatelessWidget { |
|
|
|
@override |
|
|
|
Widget build(BuildContext context) { |
|
|
|
final controller = Get.find<AgentController>(); |
|
|
|
final theme = Theme.of(context); |
|
|
|
final primaryColor = theme.primaryColor; |
|
|
|
final backgroundColor = theme.scaffoldBackgroundColor; |
|
|
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
|
|
|
|
|
|
|
return Scaffold( |
|
|
|
appBar: AppBar( |
|
|
|
elevation: 1, |
|
|
|
backgroundColor: backgroundColor, |
|
|
|
leading: IconButton( |
|
|
|
icon: Icon(Icons.arrow_back_ios, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
size: 20), |
|
|
|
onPressed: () => Get.back(), |
|
|
|
), |
|
|
|
titleSpacing: 0, |
|
|
|
title: Row( |
|
|
|
children: [ |
|
|
|
Container( |
|
|
|
width: 40, |
|
|
|
height: 40, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Theme.of(context).primaryColor, |
|
|
|
color: primaryColor, |
|
|
|
borderRadius: BorderRadius.circular(20), |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: Colors.black.withOpacity(0.1), |
|
|
|
blurRadius: 3, |
|
|
|
offset: const Offset(0, 1), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: const Icon( |
|
|
|
Icons.smart_toy_outlined, |
|
|
|
color: Colors.white, |
|
|
|
size: 22, |
|
|
|
), |
|
|
|
), |
|
|
|
const SizedBox(width: 10), |
|
|
|
const SizedBox(width: 12), |
|
|
|
Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
const Text( |
|
|
|
Text( |
|
|
|
'DeepVoice AI', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 16, |
|
|
|
fontWeight: FontWeight.bold, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
), |
|
|
|
), |
|
|
|
Row( |
|
|
|
children: [ |
|
|
|
Container( |
|
|
|
width: 8, |
|
|
|
height: 8, |
|
|
|
decoration: BoxDecoration( |
|
|
|
width: 6, |
|
|
|
height: 6, |
|
|
|
decoration: const BoxDecoration( |
|
|
|
color: Colors.green, |
|
|
|
borderRadius: BorderRadius.circular(4), |
|
|
|
shape: BoxShape.circle, |
|
|
|
), |
|
|
|
), |
|
|
|
const SizedBox(width: 4), |
|
|
|
@ -54,6 +76,7 @@ class AgentView extends StatelessWidget { |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 12, |
|
|
|
color: Colors.green, |
|
|
|
fontWeight: FontWeight.w500, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
@ -63,15 +86,63 @@ class AgentView extends StatelessWidget { |
|
|
|
], |
|
|
|
), |
|
|
|
actions: [ |
|
|
|
PopupMenuButton( |
|
|
|
icon: const Icon(Icons.more_vert), |
|
|
|
// TTS开关 |
|
|
|
Obx(() => IconButton( |
|
|
|
icon: Icon( |
|
|
|
controller.isTtsEnabled.value |
|
|
|
? Icons.volume_up |
|
|
|
: Icons.volume_off, |
|
|
|
color: controller.isTtsEnabled.value |
|
|
|
? primaryColor |
|
|
|
: isDarkMode |
|
|
|
? Colors.grey[400] |
|
|
|
: Colors.grey, |
|
|
|
), |
|
|
|
onPressed: () => controller.toggleTts(), |
|
|
|
)), |
|
|
|
|
|
|
|
// 更多选项 |
|
|
|
Container( |
|
|
|
width: 34, |
|
|
|
height: 34, |
|
|
|
margin: const EdgeInsets.only(right: 8), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], |
|
|
|
shape: BoxShape.circle, |
|
|
|
boxShadow: [ |
|
|
|
if (!isDarkMode) |
|
|
|
BoxShadow( |
|
|
|
color: Colors.black.withOpacity(0.05), |
|
|
|
blurRadius: 2, |
|
|
|
spreadRadius: 0.5, |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: PopupMenuButton( |
|
|
|
icon: Icon(Icons.more_vert, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
size: 20), |
|
|
|
shape: RoundedRectangleBorder( |
|
|
|
borderRadius: BorderRadius.circular(12), |
|
|
|
), |
|
|
|
color: isDarkMode ? Colors.grey[800] : Colors.white, |
|
|
|
itemBuilder: (context) => [ |
|
|
|
PopupMenuItem( |
|
|
|
child: const Text('清除聊天记录'), |
|
|
|
onTap: () => controller.clearChat(), |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
Icon(Icons.delete_outline, color: primaryColor, size: 20), |
|
|
|
const SizedBox(width: 8), |
|
|
|
const Text('清除聊天记录', style: TextStyle(fontSize: 14)), |
|
|
|
], |
|
|
|
), |
|
|
|
onTap: () => Future.delayed( |
|
|
|
const Duration(milliseconds: 100), |
|
|
|
() => controller.clearChat(), |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
body: Column( |
|
|
|
@ -81,12 +152,14 @@ class AgentView extends StatelessWidget { |
|
|
|
child: Obx( |
|
|
|
() => ListView.builder( |
|
|
|
controller: controller.scrollController, |
|
|
|
reverse: false, |
|
|
|
padding: const EdgeInsets.all(16), |
|
|
|
reverse: true, |
|
|
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), |
|
|
|
itemCount: controller.messages.length, |
|
|
|
itemBuilder: (context, index) { |
|
|
|
final message = controller.messages[index]; |
|
|
|
return MessageBubble( |
|
|
|
final message = controller.messages[controller.messages.length - 1 - index]; |
|
|
|
return Padding( |
|
|
|
padding: const EdgeInsets.only(bottom: 8), |
|
|
|
child: MessageBubble( |
|
|
|
isUser: message.isUser, |
|
|
|
message: message.text, |
|
|
|
timestamp: message.timestamp, |
|
|
|
@ -94,6 +167,7 @@ class AgentView extends StatelessWidget { |
|
|
|
hasImage: message.hasImage, |
|
|
|
imagePath: message.imagePath, |
|
|
|
mate: message.meta, |
|
|
|
), |
|
|
|
); |
|
|
|
}, |
|
|
|
), |
|
|
|
@ -102,16 +176,22 @@ class AgentView extends StatelessWidget { |
|
|
|
|
|
|
|
// 输入区域 |
|
|
|
Container( |
|
|
|
padding: const EdgeInsets.all(16), |
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Theme.of(context).scaffoldBackgroundColor, |
|
|
|
color: backgroundColor, |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: Colors.black.withOpacity(0.05), |
|
|
|
blurRadius: 5, |
|
|
|
color: Colors.black.withOpacity(0.06), |
|
|
|
blurRadius: 8, |
|
|
|
offset: const Offset(0, -1), |
|
|
|
), |
|
|
|
], |
|
|
|
border: Border( |
|
|
|
top: BorderSide( |
|
|
|
color: Colors.grey.withOpacity(0.15), |
|
|
|
width: 1, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
child: Column( |
|
|
|
mainAxisSize: MainAxisSize.min, |
|
|
|
@ -141,12 +221,12 @@ class AgentView extends StatelessWidget { |
|
|
|
child: GestureDetector( |
|
|
|
onTap: () => controller.clearImageInput(), |
|
|
|
child: Container( |
|
|
|
padding: EdgeInsets.all(4), |
|
|
|
padding: const EdgeInsets.all(4), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.black.withOpacity(0.6), |
|
|
|
shape: BoxShape.circle, |
|
|
|
), |
|
|
|
child: Icon( |
|
|
|
child: const Icon( |
|
|
|
Icons.close, |
|
|
|
color: Colors.white, |
|
|
|
size: 16, |
|
|
|
@ -158,7 +238,7 @@ class AgentView extends StatelessWidget { |
|
|
|
), |
|
|
|
|
|
|
|
// 预设提示选项 |
|
|
|
SizedBox(height: 8), |
|
|
|
const SizedBox(height: 12), |
|
|
|
SingleChildScrollView( |
|
|
|
scrollDirection: Axis.horizontal, |
|
|
|
child: Row( |
|
|
|
@ -168,13 +248,13 @@ class AgentView extends StatelessWidget { |
|
|
|
label: '总结图片内容', |
|
|
|
onTap: () => controller.summarizeImage(), |
|
|
|
), |
|
|
|
SizedBox(width: 8), |
|
|
|
const SizedBox(width: 8), |
|
|
|
_buildPromptChip( |
|
|
|
context: context, |
|
|
|
label: '抽取图像文字', |
|
|
|
onTap: () => controller.extractTextFromImage(), |
|
|
|
), |
|
|
|
SizedBox(width: 8), |
|
|
|
const SizedBox(width: 8), |
|
|
|
_buildPromptChip( |
|
|
|
context: context, |
|
|
|
label: '翻译图像文字', |
|
|
|
@ -183,11 +263,11 @@ class AgentView extends StatelessWidget { |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
SizedBox(height: 12), |
|
|
|
const SizedBox(height: 12), |
|
|
|
], |
|
|
|
); |
|
|
|
} else { |
|
|
|
return SizedBox.shrink(); |
|
|
|
return const SizedBox.shrink(); |
|
|
|
} |
|
|
|
}), |
|
|
|
|
|
|
|
@ -202,15 +282,16 @@ class AgentView extends StatelessWidget { |
|
|
|
controller.toggleInputMode(); |
|
|
|
}, |
|
|
|
child: Container( |
|
|
|
width: 48, |
|
|
|
height: 48, |
|
|
|
width: 44, |
|
|
|
height: 44, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.grey[200], |
|
|
|
borderRadius: BorderRadius.circular(24), |
|
|
|
borderRadius: BorderRadius.circular(22), |
|
|
|
), |
|
|
|
child: Icon( |
|
|
|
isTextMode ? Icons.mic_none : Icons.keyboard, |
|
|
|
color: Colors.grey[600], |
|
|
|
color: Colors.grey[700], |
|
|
|
size: 22, |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
@ -229,7 +310,11 @@ class AgentView extends StatelessWidget { |
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.grey[200], |
|
|
|
borderRadius: BorderRadius.circular(24), |
|
|
|
borderRadius: BorderRadius.circular(22), |
|
|
|
border: Border.all( |
|
|
|
color: Colors.grey.withOpacity(0.1), |
|
|
|
width: 1.0, |
|
|
|
), |
|
|
|
), |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
@ -243,10 +328,18 @@ class AgentView extends StatelessWidget { |
|
|
|
controller: controller.textController, |
|
|
|
maxLines: isImageActive ? 2 : 1, |
|
|
|
minLines: isImageActive ? 2 : 1, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 15, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
), |
|
|
|
decoration: InputDecoration( |
|
|
|
hintText: isImageActive |
|
|
|
? '添加对图片的问题或描述...' |
|
|
|
: '输入消息...', |
|
|
|
hintStyle: TextStyle( |
|
|
|
color: Colors.grey[500], |
|
|
|
fontSize: 15, |
|
|
|
), |
|
|
|
border: InputBorder.none, |
|
|
|
contentPadding: EdgeInsets.symmetric( |
|
|
|
vertical: isImageActive ? 12 : 0), |
|
|
|
@ -258,9 +351,13 @@ class AgentView extends StatelessWidget { |
|
|
|
), |
|
|
|
GestureDetector( |
|
|
|
onTap: () => controller.sendTextMessage(), |
|
|
|
child: Container( |
|
|
|
padding: const EdgeInsets.all(6), |
|
|
|
child: Icon( |
|
|
|
Icons.send, |
|
|
|
color: Theme.of(context).primaryColor, |
|
|
|
color: primaryColor, |
|
|
|
size: 22, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
@ -277,12 +374,16 @@ class AgentView extends StatelessWidget { |
|
|
|
} |
|
|
|
}, |
|
|
|
child: Container( |
|
|
|
height: 48, |
|
|
|
height: 44, |
|
|
|
padding: |
|
|
|
const EdgeInsets.symmetric(horizontal: 16), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.grey[200], |
|
|
|
borderRadius: BorderRadius.circular(24), |
|
|
|
borderRadius: BorderRadius.circular(22), |
|
|
|
border: Border.all( |
|
|
|
color: Colors.grey.withOpacity(0.1), |
|
|
|
width: 1.0, |
|
|
|
), |
|
|
|
), |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
@ -306,19 +407,18 @@ class AgentView extends StatelessWidget { |
|
|
|
Icons.mic, |
|
|
|
size: 14, |
|
|
|
color: isListening |
|
|
|
? Theme.of(context) |
|
|
|
.primaryColor |
|
|
|
: Colors.grey, |
|
|
|
? primaryColor |
|
|
|
: Colors.grey[500], |
|
|
|
), |
|
|
|
const SizedBox(width: 4), |
|
|
|
Text( |
|
|
|
'正在聆听', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 12, |
|
|
|
fontSize: 13, |
|
|
|
fontWeight: FontWeight.w500, |
|
|
|
color: isListening |
|
|
|
? Theme.of(context) |
|
|
|
.primaryColor |
|
|
|
: Colors.grey, |
|
|
|
? primaryColor |
|
|
|
: Colors.grey[500], |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
@ -339,18 +439,22 @@ class AgentView extends StatelessWidget { |
|
|
|
|
|
|
|
// 图片选择按钮 |
|
|
|
Container( |
|
|
|
width: 48, |
|
|
|
height: 48, |
|
|
|
width: 44, |
|
|
|
height: 44, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.grey[200], |
|
|
|
borderRadius: BorderRadius.circular(24), |
|
|
|
borderRadius: BorderRadius.circular(22), |
|
|
|
), |
|
|
|
child: PopupMenuButton<String>( |
|
|
|
icon: Icon( |
|
|
|
Icons.add_photo_alternate_outlined, |
|
|
|
color: Colors.grey[600], |
|
|
|
color: Colors.grey[700], |
|
|
|
size: 22, |
|
|
|
), |
|
|
|
padding: EdgeInsets.zero, |
|
|
|
shape: RoundedRectangleBorder( |
|
|
|
borderRadius: BorderRadius.circular(12), |
|
|
|
), |
|
|
|
onSelected: (value) async { |
|
|
|
if (value == 'camera') { |
|
|
|
await controller.takePhoto(); |
|
|
|
@ -364,9 +468,9 @@ class AgentView extends StatelessWidget { |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
Icon(Icons.camera_alt, |
|
|
|
color: Theme.of(context).primaryColor), |
|
|
|
SizedBox(width: 8), |
|
|
|
Text('拍照'), |
|
|
|
color: primaryColor, size: 20), |
|
|
|
const SizedBox(width: 8), |
|
|
|
const Text('拍照', style: TextStyle(fontSize: 14)), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
@ -375,9 +479,9 @@ class AgentView extends StatelessWidget { |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
Icon(Icons.photo_library, |
|
|
|
color: Theme.of(context).primaryColor), |
|
|
|
SizedBox(width: 8), |
|
|
|
Text('从相册选择'), |
|
|
|
color: primaryColor, size: 20), |
|
|
|
const SizedBox(width: 8), |
|
|
|
const Text('从相册选择', style: TextStyle(fontSize: 14)), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
@ -400,22 +504,26 @@ class AgentView extends StatelessWidget { |
|
|
|
required String label, |
|
|
|
required VoidCallback onTap, |
|
|
|
}) { |
|
|
|
final theme = Theme.of(context); |
|
|
|
final primaryColor = theme.primaryColor; |
|
|
|
|
|
|
|
return GestureDetector( |
|
|
|
onTap: onTap, |
|
|
|
child: Container( |
|
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), |
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Theme.of(context).primaryColor.withOpacity(0.1), |
|
|
|
color: primaryColor.withOpacity(0.08), |
|
|
|
borderRadius: BorderRadius.circular(16), |
|
|
|
border: Border.all( |
|
|
|
color: Theme.of(context).primaryColor.withOpacity(0.3), |
|
|
|
color: primaryColor.withOpacity(0.2), |
|
|
|
width: 1, |
|
|
|
), |
|
|
|
), |
|
|
|
child: Text( |
|
|
|
label, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 13, |
|
|
|
color: Theme.of(context).primaryColor, |
|
|
|
color: primaryColor, |
|
|
|
fontWeight: FontWeight.w500, |
|
|
|
), |
|
|
|
), |
|
|
|
@ -463,7 +571,7 @@ class _SoundBarState extends State<_SoundBar> |
|
|
|
duration: const Duration(milliseconds: 500), |
|
|
|
); |
|
|
|
|
|
|
|
_animation = Tween<double>(begin: 3, end: 15).animate( |
|
|
|
_animation = Tween<double>(begin: 3, end: 18).animate( |
|
|
|
CurvedAnimation( |
|
|
|
parent: _controller, |
|
|
|
curve: Curves.easeInOut, |
|
|
|
@ -493,7 +601,7 @@ class _SoundBarState extends State<_SoundBar> |
|
|
|
} else { |
|
|
|
_controller.reset(); |
|
|
|
// 设置为小高度的静态状态 |
|
|
|
_controller.animateTo(0.1, duration: Duration(milliseconds: 300)); |
|
|
|
_controller.animateTo(0.1, duration: const Duration(milliseconds: 300)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -505,13 +613,17 @@ class _SoundBarState extends State<_SoundBar> |
|
|
|
|
|
|
|
@override |
|
|
|
Widget build(BuildContext context) { |
|
|
|
final primaryColor = Theme.of(context).primaryColor; |
|
|
|
|
|
|
|
return Container( |
|
|
|
width: 2, |
|
|
|
width: 2.5, |
|
|
|
height: widget.isActive ? _animation.value : 3, |
|
|
|
margin: const EdgeInsets.symmetric(horizontal: 1), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: |
|
|
|
widget.isActive ? Theme.of(context).primaryColor : Colors.grey[400], |
|
|
|
borderRadius: BorderRadius.circular(1), |
|
|
|
color: widget.isActive |
|
|
|
? primaryColor.withOpacity(0.8) |
|
|
|
: Colors.grey.withOpacity(0.4), |
|
|
|
borderRadius: BorderRadius.circular(1.5), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|