|
|
|
@ -1,3 +1,4 @@ |
|
|
|
import 'dart:io'; |
|
|
|
import 'package:flutter/material.dart'; |
|
|
|
import 'package:get/get.dart'; |
|
|
|
import '../controllers/agent_controller.dart'; |
|
|
|
@ -90,6 +91,8 @@ class AgentView extends StatelessWidget { |
|
|
|
message: message.text, |
|
|
|
timestamp: message.timestamp, |
|
|
|
isRecognizing: message.isRecognizing, |
|
|
|
hasImage: message.hasImage, |
|
|
|
imagePath: message.imagePath, |
|
|
|
); |
|
|
|
}, |
|
|
|
), |
|
|
|
@ -109,145 +112,265 @@ class AgentView extends StatelessWidget { |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: Row( |
|
|
|
child: Column( |
|
|
|
mainAxisSize: MainAxisSize.min, |
|
|
|
children: [ |
|
|
|
// 键盘/语音切换按钮 |
|
|
|
// 图片预览区域 - 仅在图片输入模式下显示 |
|
|
|
Obx(() { |
|
|
|
final isTextMode = controller.isTextInputMode.value; |
|
|
|
return GestureDetector( |
|
|
|
onTap: () { |
|
|
|
controller.toggleInputMode(); |
|
|
|
}, |
|
|
|
child: Container( |
|
|
|
width: 48, |
|
|
|
height: 48, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.grey[200], |
|
|
|
borderRadius: BorderRadius.circular(24), |
|
|
|
), |
|
|
|
child: Icon( |
|
|
|
isTextMode ? Icons.mic_none : Icons.keyboard, |
|
|
|
color: Colors.grey[600], |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
}), |
|
|
|
|
|
|
|
const SizedBox(width: 8), |
|
|
|
|
|
|
|
// 文本输入框或语音波形图 |
|
|
|
Expanded( |
|
|
|
child: Obx(() { |
|
|
|
final isTextMode = controller.isTextInputMode.value; |
|
|
|
|
|
|
|
if (isTextMode) { |
|
|
|
// 文本输入模式 |
|
|
|
return Container( |
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.grey[200], |
|
|
|
borderRadius: BorderRadius.circular(24), |
|
|
|
), |
|
|
|
child: Row( |
|
|
|
if (controller.isImageInputActive.value && controller.selectedImagePath.value != null) { |
|
|
|
return Column( |
|
|
|
mainAxisSize: MainAxisSize.min, |
|
|
|
children: [ |
|
|
|
// 图片预览和关闭按钮 |
|
|
|
Stack( |
|
|
|
children: [ |
|
|
|
Expanded( |
|
|
|
child: TextField( |
|
|
|
controller: controller.textController, |
|
|
|
decoration: const InputDecoration( |
|
|
|
hintText: '输入消息...', |
|
|
|
border: InputBorder.none, |
|
|
|
), |
|
|
|
onSubmitted: (_) => controller.sendTextMessage(), |
|
|
|
ClipRRect( |
|
|
|
borderRadius: BorderRadius.circular(12), |
|
|
|
child: Image.file( |
|
|
|
File(controller.selectedImagePath.value!), |
|
|
|
height: 150, |
|
|
|
width: double.infinity, |
|
|
|
fit: BoxFit.cover, |
|
|
|
), |
|
|
|
), |
|
|
|
GestureDetector( |
|
|
|
onTap: () => controller.sendTextMessage(), |
|
|
|
child: Icon( |
|
|
|
Icons.send, |
|
|
|
color: Theme.of(context).primaryColor, |
|
|
|
Positioned( |
|
|
|
top: 8, |
|
|
|
right: 8, |
|
|
|
child: GestureDetector( |
|
|
|
onTap: () => controller.clearImageInput(), |
|
|
|
child: Container( |
|
|
|
padding: EdgeInsets.all(4), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.black.withOpacity(0.6), |
|
|
|
shape: BoxShape.circle, |
|
|
|
), |
|
|
|
child: Icon( |
|
|
|
Icons.close, |
|
|
|
color: Colors.white, |
|
|
|
size: 16, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} else { |
|
|
|
// 语音输入模式 - 始终显示波形图 |
|
|
|
|
|
|
|
// 预设提示选项 |
|
|
|
SizedBox(height: 8), |
|
|
|
SingleChildScrollView( |
|
|
|
scrollDirection: Axis.horizontal, |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
_buildPromptChip( |
|
|
|
context: context, |
|
|
|
label: '总结图片内容', |
|
|
|
onTap: () => controller.summarizeImage(), |
|
|
|
), |
|
|
|
SizedBox(width: 8), |
|
|
|
_buildPromptChip( |
|
|
|
context: context, |
|
|
|
label: '抽取图像文字', |
|
|
|
onTap: () => controller.extractTextFromImage(), |
|
|
|
), |
|
|
|
SizedBox(width: 8), |
|
|
|
_buildPromptChip( |
|
|
|
context: context, |
|
|
|
label: '翻译图像文字', |
|
|
|
onTap: () => controller.translateImageText(), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
SizedBox(height: 12), |
|
|
|
], |
|
|
|
); |
|
|
|
} else { |
|
|
|
return SizedBox.shrink(); |
|
|
|
} |
|
|
|
}), |
|
|
|
|
|
|
|
// 输入控件行 |
|
|
|
Row( |
|
|
|
children: [ |
|
|
|
// 键盘/语音切换按钮 |
|
|
|
Obx(() { |
|
|
|
final isTextMode = controller.isTextInputMode.value; |
|
|
|
return GestureDetector( |
|
|
|
onTap: () { |
|
|
|
if (controller.isListening.value) { |
|
|
|
controller.stopVoiceInput(); |
|
|
|
} else { |
|
|
|
controller.startVoiceInput(); |
|
|
|
} |
|
|
|
controller.toggleInputMode(); |
|
|
|
}, |
|
|
|
child: Container( |
|
|
|
width: 48, |
|
|
|
height: 48, |
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.grey[200], |
|
|
|
borderRadius: BorderRadius.circular(24), |
|
|
|
), |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
// 语音波形图 - 始终显示 |
|
|
|
Expanded( |
|
|
|
child: Row( |
|
|
|
mainAxisAlignment: MainAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
...List.generate(10, (index) { |
|
|
|
return _buildSoundBar(index); |
|
|
|
}), |
|
|
|
const Spacer(), |
|
|
|
// "正在聆听"文字 |
|
|
|
Obx(() { |
|
|
|
final isListening = controller.isListening.value; |
|
|
|
return Row( |
|
|
|
children: [ |
|
|
|
Icon( |
|
|
|
Icons.mic, |
|
|
|
size: 14, |
|
|
|
color: isListening |
|
|
|
? Theme.of(context).primaryColor |
|
|
|
: Colors.grey, |
|
|
|
), |
|
|
|
const SizedBox(width: 4), |
|
|
|
Text( |
|
|
|
'正在聆听', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 12, |
|
|
|
color: isListening |
|
|
|
? Theme.of(context).primaryColor |
|
|
|
: Colors.grey, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
); |
|
|
|
}), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
child: Icon( |
|
|
|
isTextMode ? Icons.mic_none : Icons.keyboard, |
|
|
|
color: Colors.grey[600], |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
}), |
|
|
|
), |
|
|
|
|
|
|
|
const SizedBox(width: 8), |
|
|
|
|
|
|
|
// 只显示添加按钮,不再显示停止按钮 |
|
|
|
Container( |
|
|
|
width: 48, |
|
|
|
height: 48, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.grey[200], |
|
|
|
borderRadius: BorderRadius.circular(24), |
|
|
|
), |
|
|
|
child: Icon( |
|
|
|
Icons.add, |
|
|
|
color: Colors.grey[600], |
|
|
|
), |
|
|
|
}), |
|
|
|
|
|
|
|
const SizedBox(width: 8), |
|
|
|
|
|
|
|
// 文本输入框或语音波形图 |
|
|
|
Expanded( |
|
|
|
child: Obx(() { |
|
|
|
final isTextMode = controller.isTextInputMode.value; |
|
|
|
|
|
|
|
if (isTextMode) { |
|
|
|
// 文本输入模式 - 注意观察图片模式时的高度变化 |
|
|
|
return Container( |
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.grey[200], |
|
|
|
borderRadius: BorderRadius.circular(24), |
|
|
|
), |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
Expanded( |
|
|
|
child: Obx(() { |
|
|
|
// 图片模式下使用更高的输入框 |
|
|
|
final isImageActive = controller.isImageInputActive.value; |
|
|
|
|
|
|
|
return TextField( |
|
|
|
controller: controller.textController, |
|
|
|
maxLines: isImageActive ? 2 : 1, |
|
|
|
minLines: isImageActive ? 2 : 1, |
|
|
|
decoration: InputDecoration( |
|
|
|
hintText: isImageActive ? '添加对图片的问题或描述...' : '输入消息...', |
|
|
|
border: InputBorder.none, |
|
|
|
contentPadding: EdgeInsets.symmetric(vertical: isImageActive ? 12 : 0), |
|
|
|
), |
|
|
|
onSubmitted: (_) => controller.sendTextMessage(), |
|
|
|
); |
|
|
|
}), |
|
|
|
), |
|
|
|
GestureDetector( |
|
|
|
onTap: () => controller.sendTextMessage(), |
|
|
|
child: Icon( |
|
|
|
Icons.send, |
|
|
|
color: Theme.of(context).primaryColor, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} else { |
|
|
|
// 语音输入模式 - 始终显示波形图 |
|
|
|
return GestureDetector( |
|
|
|
onTap: () { |
|
|
|
if (controller.isListening.value) { |
|
|
|
controller.stopVoiceInput(); |
|
|
|
} else { |
|
|
|
controller.startVoiceInput(); |
|
|
|
} |
|
|
|
}, |
|
|
|
child: Container( |
|
|
|
height: 48, |
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.grey[200], |
|
|
|
borderRadius: BorderRadius.circular(24), |
|
|
|
), |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
// 语音波形图 - 始终显示 |
|
|
|
Expanded( |
|
|
|
child: Row( |
|
|
|
mainAxisAlignment: MainAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
...List.generate(10, (index) { |
|
|
|
return _buildSoundBar(index); |
|
|
|
}), |
|
|
|
const Spacer(), |
|
|
|
// "正在聆听"文字 |
|
|
|
Obx(() { |
|
|
|
final isListening = controller.isListening.value; |
|
|
|
return Row( |
|
|
|
children: [ |
|
|
|
Icon( |
|
|
|
Icons.mic, |
|
|
|
size: 14, |
|
|
|
color: isListening |
|
|
|
? Theme.of(context).primaryColor |
|
|
|
: Colors.grey, |
|
|
|
), |
|
|
|
const SizedBox(width: 4), |
|
|
|
Text( |
|
|
|
'正在聆听', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 12, |
|
|
|
color: isListening |
|
|
|
? Theme.of(context).primaryColor |
|
|
|
: Colors.grey, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
); |
|
|
|
}), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
}), |
|
|
|
), |
|
|
|
|
|
|
|
const SizedBox(width: 8), |
|
|
|
|
|
|
|
// 图片选择按钮 |
|
|
|
Container( |
|
|
|
width: 48, |
|
|
|
height: 48, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.grey[200], |
|
|
|
borderRadius: BorderRadius.circular(24), |
|
|
|
), |
|
|
|
child: PopupMenuButton<String>( |
|
|
|
icon: Icon( |
|
|
|
Icons.add_photo_alternate_outlined, |
|
|
|
color: Colors.grey[600], |
|
|
|
), |
|
|
|
padding: EdgeInsets.zero, |
|
|
|
onSelected: (value) async { |
|
|
|
if (value == 'camera') { |
|
|
|
await controller.takePhoto(); |
|
|
|
} else if (value == 'gallery') { |
|
|
|
await controller.pickImage(); |
|
|
|
} |
|
|
|
}, |
|
|
|
itemBuilder: (context) => [ |
|
|
|
PopupMenuItem<String>( |
|
|
|
value: 'camera', |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
Icon(Icons.camera_alt, color: Theme.of(context).primaryColor), |
|
|
|
SizedBox(width: 8), |
|
|
|
Text('拍照'), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
PopupMenuItem<String>( |
|
|
|
value: 'gallery', |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
Icon(Icons.photo_library, color: Theme.of(context).primaryColor), |
|
|
|
SizedBox(width: 8), |
|
|
|
Text('从相册选择'), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
@ -257,6 +380,35 @@ class AgentView extends StatelessWidget { |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建提示选项芯片 |
|
|
|
Widget _buildPromptChip({ |
|
|
|
required BuildContext context, |
|
|
|
required String label, |
|
|
|
required VoidCallback onTap, |
|
|
|
}) { |
|
|
|
return GestureDetector( |
|
|
|
onTap: onTap, |
|
|
|
child: Container( |
|
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Theme.of(context).primaryColor.withOpacity(0.1), |
|
|
|
borderRadius: BorderRadius.circular(16), |
|
|
|
border: Border.all( |
|
|
|
color: Theme.of(context).primaryColor.withOpacity(0.3), |
|
|
|
), |
|
|
|
), |
|
|
|
child: Text( |
|
|
|
label, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 13, |
|
|
|
color: Theme.of(context).primaryColor, |
|
|
|
fontWeight: FontWeight.w500, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建声音柱 |
|
|
|
Widget _buildSoundBar(int index) { |
|
|
|
return Padding( |
|
|
|
|