Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
1eb437cc2c
  1. 1
      .gitignore
  2. 44
      lib/data/services/deapsound_ai_service.dart
  3. 4
      lib/modules/agent/bindings/agent_binding.dart
  4. 58
      lib/modules/agent/controllers/agent_controller.dart
  5. 402
      lib/modules/agent/controllers/message_action_controller.dart
  6. 80
      lib/modules/agent/controllers/report_controller.dart
  7. 599
      lib/modules/agent/views/agent_view.dart
  8. 40
      lib/modules/agent/views/message_bubble.dart
  9. 143
      lib/modules/agent/widgets/sound_bar_widget.dart
  10. 209
      lib/modules/meeting/views/ai_bottom_sheet.dart

1
.gitignore

@ -222,3 +222,4 @@ fastlane/Preview.html
fastlane/screenshots/**/*.png fastlane/screenshots/**/*.png
fastlane/test_output fastlane/test_output
fastlane/readme.md fastlane/readme.md
.trae/rules/project_rules.md

44
lib/data/services/deapsound_ai_service.dart

@ -2,7 +2,6 @@ import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../../core/utils/logger.dart';
import '../models/appconfig.dart'; import '../models/appconfig.dart';
import 'ai_service.dart'; import 'ai_service.dart';
@ -60,27 +59,18 @@ class DeapsoundAIService implements AiService {
String? userProperties, String? userProperties,
}) async* { }) async* {
final client = http.Client(); final client = http.Client();
final url = Uri.parse('https://api.deapsound.com/ai/funcchat'); final url = Uri.parse(baseUrl);
List contextsList = [];
String msg = '';
for (var i = 0; i < messages.length; i++) {
Map item = messages[i];
if (i < messages.length - 1) {
if (item['role'] == 'user') {
contextsList.add(item['content']);
}
} else {
msg = item['content'];
}
}
final headers = { final headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': apiKey, 'Authorization': 'Bearer $apiKey',
}; };
final requestBody = {"contexts": contextsList, "msg": msg}; final requestBody = {
"model": model,
"messages": messages,
"stream": true,
};
try { try {
final request = http.Request('POST', url) final request = http.Request('POST', url)
@ -88,7 +78,6 @@ class DeapsoundAIService implements AiService {
..body = jsonEncode(requestBody); ..body = jsonEncode(requestBody);
final response = await client.send(request); final response = await client.send(request);
//流式处理响应数据
await for (final chunk in response.stream await for (final chunk in response.stream
.transform(utf8.decoder) .transform(utf8.decoder)
.transform(const LineSplitter())) { .transform(const LineSplitter())) {
@ -96,28 +85,19 @@ class DeapsoundAIService implements AiService {
if (chunk.startsWith('data:')) { if (chunk.startsWith('data:')) {
final jsonStr = chunk.substring(5).trim(); final jsonStr = chunk.substring(5).trim();
if (jsonStr == '[DONE]') {
try {
final data = jsonDecode(jsonStr);
if (data['role'] == 'card') {
yield data['meta'];
if (data['meta']['musicaudio'] != null) {
break; break;
} }
}
if (data['role'] == 'ai') { final data = jsonDecode(jsonStr);
String content = data['content'] ?? ''; final content = data['choices'][0]['delta']['content'] ?? '';
if (content.isNotEmpty) { if (content.isNotEmpty) {
yield content; yield content;
} }
} }
} catch (e) {
Logger.error('JSON解析错误: ${e.toString()}');
}
}
} }
} catch (e) { } catch (e) {
Logger.error('请求异常: ${e.toString()}'); throw Exception('Unknown error');
} finally { } finally {
client.close(); client.close();
} }

4
lib/modules/agent/bindings/agent_binding.dart

@ -1,11 +1,11 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/agent_controller.dart'; import '../controllers/agent_controller.dart';
import '../controllers/report_controller.dart'; import '../controllers/message_action_controller.dart';
class AgentBinding extends Bindings { class AgentBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
Get.lazyPut<AgentController>(() => AgentController()); Get.lazyPut<AgentController>(() => AgentController());
Get.lazyPut<ReportController>(() => ReportController()); Get.lazyPut<MessageActionController>(() => MessageActionController());
} }
} }

58
lib/modules/agent/controllers/agent_controller.dart

@ -75,6 +75,12 @@ class AgentController extends GetxController {
final isFullScreenEditMode = false.obs; // 是否处于全屏编辑模式 final isFullScreenEditMode = false.obs; // 是否处于全屏编辑模式
final shouldShowExpandButton = false.obs; // 是否显示展开按钮 final shouldShowExpandButton = false.obs; // 是否显示展开按钮
// 图片选择选项展开状态
final isImageOptionsExpanded = false.obs; // 是否展开图片选择选项
// 键盘状态
final isKeyboardVisible = false.obs; // 键盘是否可见
// 流事件订阅 // 流事件订阅
StreamSubscription<AgentServiceEvent>? _eventSubscription; StreamSubscription<AgentServiceEvent>? _eventSubscription;
@ -898,6 +904,58 @@ class AgentController extends GetxController {
isImageInputActive.value = false; isImageInputActive.value = false;
} }
// 切换图片选项展开状态
void toggleImageOptions() {
if (isImageOptionsExpanded.value) {
// 如果当前已展开,则收缩
collapseImageOptions();
} else {
// 如果当前未展开,则展开并关闭键盘
if (isKeyboardVisible.value) {
hideKeyboard();
// 延迟展开选项,等待键盘完全关闭
Future.delayed(const Duration(milliseconds: 200), () {
// 再次检查键盘状态,确保键盘已关闭
if (!isKeyboardVisible.value) {
isImageOptionsExpanded.value = true;
} else {
// 如果键盘还没关闭,再等一会儿
Future.delayed(const Duration(milliseconds: 100), () {
if (!isKeyboardVisible.value) {
isImageOptionsExpanded.value = true;
}
});
}
});
} else {
// 键盘未显示,直接展开
isImageOptionsExpanded.value = true;
}
}
}
// 收缩图片选项
void collapseImageOptions() {
isImageOptionsExpanded.value = false;
}
// 更新键盘可见状态
void updateKeyboardVisibility(bool isVisible) {
final wasVisible = isKeyboardVisible.value;
isKeyboardVisible.value = isVisible;
// 只有当键盘从隐藏变为显示时,才收缩图片选项
// 避免键盘关闭时误触发收缩逻辑
if (!wasVisible && isVisible && isImageOptionsExpanded.value) {
collapseImageOptions();
}
}
// 隐藏键盘
void hideKeyboard() {
FocusManager.instance.primaryFocus?.unfocus();
}
// 使用预设提示词处理图片 // 使用预设提示词处理图片
void useImagePrompt(String prompt) { void useImagePrompt(String prompt) {
// 确保selectedImagePath.value不为null // 确保selectedImagePath.value不为null

402
lib/modules/agent/controllers/message_action_controller.dart

@ -0,0 +1,402 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import '../../../core/theme/app_colors.dart';
import '../../../data/models/message_model.dart';
import '../../../data/services/network/api.dart';
/// 消息操作控制器
/// 负责处理消息长按、复制、举报等操作
class MessageActionController extends GetxController {
// 当前举报的消息
final reportingMessage = Rxn<Message>();
// 选择的举报原因
List<String> selectedReasons = [];
// 举报描述
final reportDescription = ''.obs;
/// 处理消息长按事件
void onLongPressMessage(
BuildContext context,
bool isDarkMode,
LongPressStartDetails details,
Message message,
) {
// 触发触觉反馈
Haptics.vibrate(HapticsType.success);
// 计算弹出菜单位置
RenderBox? renderBox = Overlay.of(context).context.findRenderObject() as RenderBox;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx + 110, // 菜单显示位置X轴坐标
details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标
),
Offset.zero & renderBox.size,
);
// 显示弹出菜单
showMenu(
context: context,
position: position,
color: isDarkMode ? AppColors.cardBackground : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
items: _buildMenuItems(context, isDarkMode, message),
);
}
/// 构建弹出菜单项
List<PopupMenuEntry> _buildMenuItems(
BuildContext context,
bool isDarkMode,
Message message,
) {
final List<PopupMenuEntry> items = [
// 复制选项
_buildPopupMenuItem(
Icon(
Icons.copy,
size: 20.w,
color: isDarkMode ? Colors.white : Colors.black87,
),
'复制',
isDarkMode ? Colors.white : Colors.black87,
() => _copyMessage(message),
),
];
// 如果不是用户消息,添加举报选项
if (!message.isUser) {
items.addAll([
// 分隔线
PopupMenuItem(
height: 1,
enabled: false,
child: Divider(
height: 1,
thickness: 0.8,
indent: 0,
endIndent: 0,
color: isDarkMode ? Colors.grey[700] : Colors.grey[300],
),
),
// 举报选项
_buildPopupMenuItem(
Icon(
Icons.report,
size: 20.w,
color: isDarkMode ? Colors.red[700] : Colors.red,
),
'举报'.tr,
isDarkMode ? Colors.red[700]! : Colors.red,
() => _showReportForm(context, isDarkMode, message),
),
]);
}
return items;
}
/// 构建弹出菜单项
PopupMenuEntry _buildPopupMenuItem(
Icon icon,
String title,
Color color,
Function() onTap,
) {
return PopupMenuItem(
onTap: onTap,
child: SizedBox(
width: 140.w,
child: Row(
children: [
icon,
8.horizontalSpace,
Text(
title,
style: TextStyle(
color: color,
fontSize: 14.sp,
),
),
],
),
),
);
}
/// 复制消息内容
void _copyMessage(Message message) {
Clipboard.setData(ClipboardData(text: message.text)).then((_) {
Get.snackbar(
'success'.tr,
'复制成功',
backgroundColor: Colors.blue.withOpacity(0.8),
colorText: Colors.white,
);
});
}
/// 显示举报表单
void _showReportForm(BuildContext context, bool isDarkMode, Message message) {
// 初始化举报状态
initReport(message);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
),
builder: (BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: _ReportForm(isDarkMode: isDarkMode),
);
},
);
}
/// 初始化举报状态
void initReport(Message message) {
reportingMessage.value = message;
selectedReasons.clear();
reportDescription.value = '';
}
/// 切换举报原因选择状态
void toggleReason(String reason) {
if (selectedReasons.contains(reason)) {
selectedReasons.remove(reason);
} else {
selectedReasons.add(reason);
}
update();
}
/// 更新举报描述
void updateDescription(String text) {
reportDescription.value = text;
}
/// 提交举报
Future<void> submitReport() async {
if (reportingMessage.value == null) return;
// 构建举报数据
final reportData = {
'describe': reportDescription.value,
'message': [],
'reportContext': reportingMessage.value!.text,
'reporttype': selectedReasons.toString(),
};
try {
// 调用API提交举报
var response = await Api.reportAiContent(reportData);
if (response != null) {
print('提交举报响应: $response');
}
// 提交成功后重置状态
selectedReasons.clear();
reportDescription.value = '';
reportingMessage.value = null;
// 关闭举报表单
Navigator.of(Get.context!).pop();
// 显示成功通知
Get.snackbar(
'提交成功',
'举报已提交,我们会尽快处理',
backgroundColor: Colors.green.withOpacity(0.8),
colorText: Colors.white,
duration: const Duration(seconds: 2),
);
} catch (e) {
// 显示错误通知
Get.snackbar(
'提交失败',
'网络错误,请稍后重试',
backgroundColor: Colors.red.withOpacity(0.8),
colorText: Colors.white,
duration: const Duration(seconds: 2),
);
}
}
}
/// 举报表单组件
class _ReportForm extends StatelessWidget {
final bool isDarkMode;
final MessageActionController controller = Get.find<MessageActionController>();
_ReportForm({required this.isDarkMode});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.w),
child: SingleChildScrollView(
child: GetBuilder<MessageActionController>(
builder: (_) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 标题栏
Stack(
alignment: Alignment.center,
children: [
Center(
child: Text(
'举报',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
Positioned(
right: 0,
child: IconButton(
icon: Icon(
Icons.close,
color: isDarkMode ? Colors.grey[400] : Colors.black54,
),
onPressed: () => Navigator.pop(context),
),
),
],
),
// 分隔线
Container(
padding: EdgeInsets.symmetric(horizontal: 12.w),
margin: EdgeInsets.only(bottom: 10.w),
child: Divider(
height: 1,
color: Colors.grey[300],
),
),
// 举报原因选择
GetBuilder<MessageActionController>(
builder: (controller) {
return Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: [
_buildReportChip('有害/不安全', isDarkMode, controller),
_buildReportChip('色情低俗', isDarkMode, controller),
_buildReportChip('误导攻击', isDarkMode, controller),
_buildReportChip('违法犯罪', isDarkMode, controller),
_buildReportChip('隐私相关', isDarkMode, controller),
_buildReportChip('侵犯我的著作权、名誉权等', isDarkMode, controller),
_buildReportChip('其他', isDarkMode, controller),
],
);
},
),
SizedBox(height: 16.h),
// 描述输入框
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
borderRadius: BorderRadius.circular(8.r),
),
child: TextField(
maxLines: 3,
onChanged: controller.updateDescription,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
),
decoration: InputDecoration(
hintText: '我们想知道你举报的原因,你可以描述你遇到的问题。',
hintStyle: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 14.sp,
),
border: InputBorder.none,
),
),
),
SizedBox(height: 14.h),
// 提交按钮
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text('提交'),
onPressed: () => controller.submitReport(),
),
SizedBox(height: 8.h),
],
),
),
),
);
}
/// 构建举报原因芯片
Widget _buildReportChip(
String label,
bool isDarkMode,
MessageActionController controller,
) {
final isSelected = controller.selectedReasons.contains(label);
return GestureDetector(
onTap: () => controller.toggleReason(label),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
color: isSelected
? Colors.red
: (isDarkMode ? Colors.grey[800] : Colors.grey[200]),
borderRadius: BorderRadius.circular(16.r),
border: Border.all(
color: isSelected
? Colors.red[700]!
: (isDarkMode ? Colors.grey[700]! : Colors.grey[300]!),
width: isSelected ? 1.0 : 0.5,
),
),
child: Text(
label,
style: TextStyle(
color: isSelected
? Colors.white
: (isDarkMode ? Colors.white : Colors.black87),
fontSize: 14.sp,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
),
);
}
}

80
lib/modules/agent/controllers/report_controller.dart

@ -1,80 +0,0 @@
import 'package:get/get.dart';
//import 'agent_controller.dart'; // 确保导入Message模型
import 'package:flutter/material.dart';
import '../../../data/services/network/api.dart';
import '../../../data/models/message_model.dart';
class ReportController extends GetxController {
// 当前举报的消息
final reportingMessage = Rxn<Message>();
// 选择的举报原因
List<String> selectedReasons = [];
// 举报描述
final reportDescription = ''.obs;
// 初始化举报状态
void initReport(Message message) {
reportingMessage.value = message;
selectedReasons.clear();
reportDescription.value = '';
}
// 切换举报原因选择状态
void toggleReason(String reason) {
if (selectedReasons.contains(reason)) {
selectedReasons.remove(reason);
} else {
selectedReasons.add(reason);
}
update();
}
// 更新举报描述
void updateDescription(String text) {
reportDescription.value = text;
}
// 提交举报
Future<void> submitReport() async {
if (reportingMessage.value == null) return;
// 构建举报数据
final reportData = {
'describe': reportDescription.value,
// 将Message对象转换为可序列化的Map
'message': [],
// 同样处理reportContext
'reportContext': reportingMessage.value!.text,
'reporttype': selectedReasons.toString(),
};
print('提交举报数据: ${reportData}');
//调用API提交举报:
var response = await Api.reportAiContent(reportData);
if (response != null) {
print('提交举报响应: $response');
}
// 提交成功后重置状态
selectedReasons.clear();
reportDescription.value = '';
reportingMessage.value = null;
// 关闭举报表单
Navigator.of(Get.context!).pop();
// 显示成功通知
Get.snackbar(
'提交成功',
'举报已提交,我们会尽快处理',
backgroundColor: Colors.green.withOpacity(0.8),
colorText: Colors.white,
duration: const Duration(seconds: 2),
);
}
}

599
lib/modules/agent/views/agent_view.dart

@ -2,21 +2,53 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/agent_controller.dart'; import '../controllers/agent_controller.dart';
import '../controllers/report_controller.dart'; import '../controllers/message_action_controller.dart';
import 'message_bubble.dart'; import 'message_bubble.dart';
import '../widgets/full_screen_editor.dart'; import '../widgets/full_screen_editor.dart';
import 'package:flutter/services.dart'; import '../widgets/sound_bar_widget.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/theme/app_colors.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import '../../../data/models/message_model.dart';
class AgentView extends StatelessWidget { class AgentView extends StatefulWidget {
const AgentView({Key? key}) : super(key: key); const AgentView({Key? key}) : super(key: key);
@override
State<AgentView> createState() => _AgentViewState();
}
class _AgentViewState extends State<AgentView> with WidgetsBindingObserver {
late AgentController controller;
late MessageActionController messageActionController;
double _keyboardHeight = 0;
@override
void initState() {
super.initState();
controller = Get.find<AgentController>();
messageActionController = Get.find<MessageActionController>();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeMetrics() {
super.didChangeMetrics();
final bottomInset = View.of(context).viewInsets.bottom;
final newKeyboardHeight = bottomInset / View.of(context).devicePixelRatio;
if (_keyboardHeight != newKeyboardHeight) {
_keyboardHeight = newKeyboardHeight;
// 更新键盘可见状态
controller.updateKeyboardVisibility(_keyboardHeight > 0);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final controller = Get.find<AgentController>();
final theme = Theme.of(context); final theme = Theme.of(context);
final primaryColor = theme.primaryColor; final primaryColor = theme.primaryColor;
final backgroundColor = theme.scaffoldBackgroundColor; final backgroundColor = theme.scaffoldBackgroundColor;
@ -172,7 +204,7 @@ class AgentView extends StatelessWidget {
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
child: GestureDetector( child: GestureDetector(
onLongPressStart: (LongPressStartDetails details) { onLongPressStart: (LongPressStartDetails details) {
_onLongPressMessageContent( messageActionController.onLongPressMessage(
context, isDarkMode, details, message); context, isDarkMode, details, message);
}, },
child: MessageBubble( child: MessageBubble(
@ -288,6 +320,9 @@ class AgentView extends StatelessWidget {
} }
}), }),
// 输入控件区域 - 包含输入行和展开的选项
Column(
children: [
// 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐 // 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐
IntrinsicHeight( IntrinsicHeight(
child: Row( child: Row(
@ -325,7 +360,8 @@ class AgentView extends StatelessWidget {
// 文本输入框或语音波形图 // 文本输入框或语音波形图
Expanded( Expanded(
child: Obx(() { child: Obx(() {
final isTextMode = controller.isTextInputMode.value; final isTextMode =
controller.isTextInputMode.value;
if (isTextMode) { if (isTextMode) {
// 文本输入模式 - 限制最大高度并只向上扩展 // 文本输入模式 - 限制最大高度并只向上扩展
@ -334,8 +370,8 @@ class AgentView extends StatelessWidget {
minHeight: 44, // 最小高度 minHeight: 44, // 最小高度
maxHeight: 120, // 最大高度限制,防止挤压按钮 maxHeight: 120, // 最大高度限制,防止挤压按钮
), ),
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 16), horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode color: isDarkMode
? Colors.grey[800] ? Colors.grey[800]
@ -356,18 +392,29 @@ class AgentView extends StatelessWidget {
Expanded( Expanded(
child: Obx(() { child: Obx(() {
// 图片模式下使用更高的输入框 // 图片模式下使用更高的输入框
final isImageActive = final isImageActive = controller
controller.isImageInputActive.value; .isImageInputActive.value;
return TextField( return TextField(
controller: controller.textController, controller:
maxLines: controller.textController,
isImageActive ? 3 : 5, // 设置合理的最大行数 maxLines: isImageActive
? 3
: 5, // 设置合理的最大行数
minLines: isImageActive ? 2 : 1, minLines: isImageActive ? 2 : 1,
keyboardType: keyboardType: TextInputType
TextInputType.multiline, // 启用多行键盘 .multiline, // 启用多行键盘
textInputAction: textInputAction: TextInputAction
TextInputAction.newline, // 支持回车换行 .newline, // 支持回车换行
onTap: () {
// 当点击输入框时,如果图片选项已展开,则收缩
if (controller
.isImageOptionsExpanded
.value) {
controller
.collapseImageOptions();
}
},
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
color: isDarkMode color: isDarkMode
@ -392,7 +439,8 @@ class AgentView extends StatelessWidget {
width: 1.0, width: 1.0,
), ),
), ),
contentPadding: EdgeInsets.symmetric( contentPadding:
EdgeInsets.symmetric(
vertical: isImageActive vertical: isImageActive
? 12 ? 12
: 8), // 调整垂直内边距 : 8), // 调整垂直内边距
@ -417,8 +465,8 @@ class AgentView extends StatelessWidget {
}, },
child: Container( child: Container(
height: 50, height: 50,
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 16), horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode color: isDarkMode
? Colors.grey[800] ? Colors.grey[800]
@ -439,14 +487,12 @@ class AgentView extends StatelessWidget {
mainAxisAlignment: mainAxisAlignment:
MainAxisAlignment.start, MainAxisAlignment.start,
children: [ children: [
...List.generate(10, (index) { const AgentSoundBarsWidget(),
return _buildSoundBar(index);
}),
const Spacer(), const Spacer(),
// "正在聆听"文字 // "正在聆听"文字
Obx(() { Obx(() {
final isListening = final isListening = controller
controller.isListening.value; .isListening.value;
return Row( return Row(
children: [ children: [
Icon( Icon(
@ -461,7 +507,8 @@ class AgentView extends StatelessWidget {
'正在聆听', '正在聆听',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w500, fontWeight:
FontWeight.w500,
color: isListening color: isListening
? primaryColor ? primaryColor
: Colors.grey[500], : Colors.grey[500],
@ -486,7 +533,8 @@ class AgentView extends StatelessWidget {
// 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮
Obx(() { Obx(() {
final hasText = controller.hasTextInput.value; final hasText = controller.hasTextInput.value;
final hasImage = controller.isImageInputActive.value; final hasImage =
controller.isImageInputActive.value;
final shouldShowSend = hasText || hasImage; final shouldShowSend = hasText || hasImage;
final shouldShowExpand = final shouldShowExpand =
controller.shouldShowExpandButton.value; controller.shouldShowExpandButton.value;
@ -499,7 +547,8 @@ class AgentView extends StatelessWidget {
// 展开按钮(在发送按钮上方) // 展开按钮(在发送按钮上方)
if (shouldShowExpand && hasText) if (shouldShowExpand && hasText)
Padding( Padding(
padding: const EdgeInsets.only(bottom: 40), padding:
const EdgeInsets.only(bottom: 40),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
// 打开底部弹出编辑模式 // 打开底部弹出编辑模式
@ -522,7 +571,8 @@ class AgentView extends StatelessWidget {
color: isDarkMode color: isDarkMode
? Colors.grey[700] ? Colors.grey[700]
: Colors.grey[300], : Colors.grey[300],
borderRadius: BorderRadius.circular(16), borderRadius:
BorderRadius.circular(16),
), ),
child: Icon( child: Icon(
Icons.expand_less, Icons.expand_less,
@ -554,8 +604,15 @@ class AgentView extends StatelessWidget {
], ],
); );
} else { } else {
// 显示图片选择按钮 // 显示'+'选择按钮
return Container( return Obx(() {
final isExpanded =
controller.isImageOptionsExpanded.value;
return GestureDetector(
onTap: () {
controller.toggleImageOptions();
},
child: Container(
width: 44, width: 44,
height: 44, height: 44,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -564,58 +621,89 @@ class AgentView extends StatelessWidget {
: Colors.grey[200], : Colors.grey[200],
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(22),
), ),
child: PopupMenuButton<String>( child: Icon(
icon: Icon( isExpanded ? Icons.close : Icons.add,
Icons.add_photo_alternate_outlined,
color: isDarkMode color: isDarkMode
? Colors.grey[300] ? Colors.grey[300]
: Colors.grey[700], : Colors.grey[700],
size: 22, size: 22,
), ),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
), ),
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: primaryColor, size: 20),
const SizedBox(width: 8),
const Text('拍照',
style: TextStyle(fontSize: 14)),
], ],
), ),
), ),
PopupMenuItem<String>(
value: 'gallery', // 展开的'+'选择选项
Obx(() {
final isExpanded =
controller.isImageOptionsExpanded.value;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: isExpanded ? 100.h : 0, // 调整高度以适应按钮尺寸
child: isExpanded
? Container(
margin: EdgeInsets.only(top: 8.h),
padding: EdgeInsets.symmetric(horizontal:16.w), // 添加左右边距
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 改为均匀分布
children: [ children: [
Icon(Icons.photo_library, // 相机按钮
color: primaryColor, size: 20), _buildPlusOptionButton(
const SizedBox(width: 8), context: context,
const Text('从相册选择', icon: Icons.camera_alt,
style: TextStyle(fontSize: 14)), label: '拍照',
], onTap: () async {
//controller.collapseImageOptions();
await controller.takePhoto();
},
isDarkMode: isDarkMode,
), ),
// 相册按钮
_buildPlusOptionButton(
context: context,
icon: Icons.photo_library,
label: '相册',
onTap: () async {
//controller.collapseImageOptions();
await controller.pickImage();
},
isDarkMode: isDarkMode,
),
// 文件按钮(预留位置)
// _buildPlusOptionButton(
// context: context,
// icon: Icons.folder,
// label: '文件',
// onTap: () {
// //controller.collapseImageOptions();
// // TODO: 实现文件选择功能
// },
// isDarkMode: isDarkMode,
// ),
// 通话按钮(预留位置)
_buildPlusOptionButton(
context: context,
icon: Icons.phone,
label: '打电话',
onTap: () {
//controller.collapseImageOptions();
// TODO: 实现通话功能
},
isDarkMode: isDarkMode,
), ),
], ],
), ),
)
: const SizedBox.shrink(),
); );
}
}), }),
], ],
), ),
),
], ],
), ),
), ),
@ -624,130 +712,11 @@ class AgentView extends StatelessWidget {
); );
} }
void _onLongPressMessageContent(BuildContext context, bool isDarkMode,
LongPressStartDetails details, Message message) {
Haptics.vibrate(HapticsType.success);
RenderBox? renderBox =
Overlay.of(context).context.findRenderObject() as RenderBox;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx + 110, // 菜单显示位置X轴坐标
details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标
),
Offset.zero & renderBox.size,
);
showMenu(
context: context,
position: position,
color: isDarkMode ? AppColors.cardBackground : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
items: [
_popupMenuEntry(
Icon(
Icons.copy,
size: 20.w,
color: isDarkMode ? Colors.white : Colors.black87,
),
'复制',
isDarkMode ? Colors.white : Colors.black87,
() {
Clipboard.setData(
ClipboardData(
text: message.text,
),
).then((value) => Get.snackbar(
'success'.tr, // 成功
'复制成功',
backgroundColor: Colors.blue.withOpacity(0.8),
colorText: Colors.white,
));
},
),
if (!message.isUser) ...[
// 自定义分隔线样式
PopupMenuItem(
height: 1, // 控制间距
enabled: false,
child: Divider(
height: 1,
thickness: 0.8,
indent: 0,
endIndent: 0,
color: isDarkMode ? Colors.grey[700] : Colors.grey[300],
),
),
_popupMenuEntry(
Icon(
Icons.report,
size: 20.w,
color: isDarkMode ? Colors.red[700] : Colors.red,
),
'举报'.tr, // 举报
isDarkMode ? Colors.red[700]! : Colors.red,
() {
// 实现举报功能
final reportController = Get.find<ReportController>();
reportController.initReport(message);
_showReportForm(context, isDarkMode);
},
),
]
],
);
}
// 更新_showReportForm方法
void _showReportForm(BuildContext context, bool isDarkMode) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
),
builder: (BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: _ReportForm(isDarkMode: isDarkMode),
);
},
);
}
PopupMenuEntry _popupMenuEntry(
Icon icon,
String title,
Color color,
Function() onTap,
) {
return PopupMenuItem(
onTap: onTap,
child: SizedBox(
width: 140.w,
child: Row(
children: [
icon,
8.horizontalSpace,
Text(
title,
style: TextStyle(
color: color,
fontSize: 14.sp,
),
),
],
),
),
);
}
// 构建提示选项芯片 // 构建提示选项芯片
Widget _buildPromptChip({ Widget _buildPromptChip({
@ -782,245 +751,65 @@ class AgentView extends StatelessWidget {
); );
} }
// 构建声音柱
Widget _buildSoundBar(int index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 1),
child: Obx(() => _SoundBar(
delay: index * 0.1,
isActive: Get.find<AgentController>().isListening.value,
)),
);
}
}
class _ReportForm extends StatelessWidget {
final bool isDarkMode;
final ReportController reportController = Get.find<ReportController>();
_ReportForm({required this.isDarkMode}); // 构建"+"号展开选项按钮
Widget _buildPlusOptionButton({
required BuildContext context,
required IconData icon,
required String label,
required VoidCallback onTap,
required bool isDarkMode,
}) {
final primaryColor = Theme.of(context).primaryColor;
@override return GestureDetector(
Widget build(BuildContext context) { onTap: onTap,
return Container( child: SizedBox(
padding: EdgeInsets.all(16.w), // 调整为一行四个按钮的宽度,考虑屏幕宽度和间距
child: SingleChildScrollView( width: (1.sw - 100.w) / 4, // 屏幕宽度减去左右边距,除以4个按钮
child: GetBuilder<ReportController>( child: Column(
builder: (_) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Stack( // 正方形图标容器 - 增大尺寸
alignment: Alignment.center,
children: [
Center(
child: Text(
'举报',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
Positioned(
right: 0,
child: IconButton(
icon: Icon(
Icons.close,
color: isDarkMode ? Colors.grey[400] : Colors.black54,
),
onPressed: () => Navigator.pop(context),
),
),
],
),
Container(
padding: EdgeInsets.symmetric(horizontal: 12.w),
margin: EdgeInsets.only(bottom: 10.w),
child: Divider(
height: 1,
color: Colors.grey[300],
),
),
GetBuilder<ReportController>(
builder: (controller) {
return Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: [
_buildReportChip('有害/不安全', isDarkMode, controller),
_buildReportChip('色情低俗', isDarkMode, controller),
_buildReportChip('误导攻击', isDarkMode, controller),
_buildReportChip('违法犯罪', isDarkMode, controller),
_buildReportChip('隐私相关', isDarkMode, controller),
_buildReportChip('侵犯我的著作权、名誉权等', isDarkMode, controller),
_buildReportChip('其他', isDarkMode, controller),
],
);
},
),
SizedBox(height: 16.h),
Container( Container(
padding: EdgeInsets.all(12.w), width: 50.w, // 调整为44.w以适应布局
height: 50.w, // 调整为44.w以适应布局
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200], color: isDarkMode ? Colors.grey[800] : Colors.grey[100],
borderRadius: BorderRadius.circular(8.r), borderRadius: BorderRadius.circular(12.r), // 调整为10.r
), border: Border.all(
child: TextField( color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!,
maxLines: 3, width: 1,
onChanged: reportController.updateDescription,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
),
decoration: InputDecoration(
hintText: '我们想知道你举报的原因,你可以描述你遇到的问题。',
hintStyle: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 14.sp,
),
border: InputBorder.none,
),
),
),
SizedBox(height: 14.h),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text('提交'),
onPressed: () => reportController.submitReport(),
),
SizedBox(height: 8.h),
],
),
), ),
), ),
); child: Icon(
} icon,
color: primaryColor,
Widget _buildReportChip( size: 20.sp, // 调整为18.sp
String label, bool isDarkMode, ReportController controller) {
final isSelected = reportController.selectedReasons.contains(label);
return GestureDetector(
onTap: () => reportController.toggleReason(label),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
color: isSelected
? Colors.red
: (isDarkMode ? Colors.grey[800] : Colors.grey[200]),
borderRadius: BorderRadius.circular(16.r),
border: Border.all(
color: isSelected
? Colors.red[700]!
: (isDarkMode ? Colors.grey[700]! : Colors.grey[300]!),
width: isSelected ? 1.0 : 0.5,
), ),
), ),
child: Text( SizedBox(height: 4.h), // 调整为3.h
// 文字标签
Text(
label, label,
style: TextStyle( style: TextStyle(
color: isSelected fontSize: 10.sp, // 调整为9.sp
? Colors.white color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
: (isDarkMode ? Colors.white : Colors.black87), fontWeight: FontWeight.w500,
fontSize: 14.sp,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
), ),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
],
), ),
);
}
}
// 声音柱动画组件
class _SoundBar extends StatefulWidget {
final double delay;
final bool isActive;
const _SoundBar({
required this.delay,
required this.isActive,
});
@override
_SoundBarState createState() => _SoundBarState();
}
class _SoundBarState extends State<_SoundBar>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_animation = Tween<double>(begin: 3, end: 18).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
), ),
)..addListener(() { );
setState(() {});
});
_startOrStopAnimation();
}
@override
void didUpdateWidget(_SoundBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isActive != oldWidget.isActive) {
_startOrStopAnimation();
} }
} }
void _startOrStopAnimation() {
if (widget.isActive) {
Future.delayed(Duration(milliseconds: (widget.delay * 1000).toInt()), () {
if (mounted && widget.isActive) {
_controller.repeat(reverse: true);
}
});
} else {
_controller.reset();
// 设置为小高度的静态状态
_controller.animateTo(0.1, duration: const Duration(milliseconds: 300));
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final primaryColor = Theme.of(context).primaryColor;
return Container(
width: 2.5,
height: widget.isActive ? _animation.value : 3,
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: BoxDecoration(
color: widget.isActive
? primaryColor.withOpacity(0.8)
: Colors.grey.withOpacity(0.4),
borderRadius: BorderRadius.circular(1.5),
),
);
}
}

40
lib/modules/agent/views/message_bubble.dart

@ -233,29 +233,33 @@ class MessageBubble extends GetView<AgentController> {
// 判断消息类型,显示不同的卡片 // 判断消息类型,显示不同的卡片
if (meta!.keys.contains('music_play')) { if (meta!.keys.contains('music_play')) {
final card_music = final card_music =
meta!["music_play"]['card_music'] as Map<String, dynamic>; meta!["music_play"]['card_music'] as Map<String, dynamic>?;
final musicId = card_music!['id']; if (card_music != null) {
final musicName = card_music!['name'] as String; final musicId = card_music['id'];
final musicUrl = card_music!['url'] as String; final musicName = card_music['name'] as String? ?? '未知歌曲';
final musicSinger = card_music!['sgener'] as String; final musicUrl = card_music['url'] as String? ?? '';
final musicImage = card_music!['image'] as String; final musicSinger = card_music['sgener'] as String? ?? '未知歌手';
final musicImage = card_music['image'] as String? ?? '';
//音乐卡片 //音乐卡片
return buildMusicItem( return buildMusicItem(
context, musicId, musicName, musicUrl, musicSinger, musicImage); context, musicId, musicName, musicUrl, musicSinger, musicImage);
}
} else if (meta!.keys.contains('music_playlist')) { } else if (meta!.keys.contains('music_playlist')) {
final card_musiclist = final card_musiclist =
meta!["music_playlist"]['card_musiclist'] as Map<String, dynamic>; meta!["music_playlist"]['card_musiclist'] as Map<String, dynamic>?;
final rawList = card_musiclist['musics'] as List; if (card_musiclist != null) {
final rawList = card_musiclist['musics'] as List? ?? [];
final playlist = final playlist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList(); rawList.map((e) => Map<String, dynamic>.from(e)).toList();
//音乐播放列表 //音乐播放列表
return buildMusicContent(context, playlist); return buildMusicContent(context, playlist);
}
} else if (meta!.keys.contains('maps_weather')) { } else if (meta!.keys.contains('maps_weather')) {
final city = meta!['maps_weather']['city'] as String; final city = meta!['maps_weather']['city'] as String? ?? '未知城市';
final rawList = meta!['maps_weather']['forecasts'] as List; final rawList = meta!['maps_weather']['forecasts'] as List? ?? [];
final daylist = final daylist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList(); rawList.map((e) => Map<String, dynamic>.from(e)).toList();
final now = daylist[0] as Map<String, dynamic>; final now = daylist.isNotEmpty ? daylist[0] : <String, dynamic>{};
//天气 //天气
return buildWeatherContent( return buildWeatherContent(
context, context,
@ -264,18 +268,19 @@ class MessageBubble extends GetView<AgentController> {
daylist, daylist,
); );
} else if (meta!.keys.contains('card_travel')) { } else if (meta!.keys.contains('card_travel')) {
final rawList = meta!['card_travel']['items'] as List; final rawList = meta!['card_travel']['items'] as List? ?? [];
final travellist = final travellist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList(); rawList.map((e) => Map<String, dynamic>.from(e)).toList();
// 旅游 // 旅游
return buildTravelContent(context, travellist); return buildTravelContent(context, travellist);
} else if (meta!.keys.contains('maps_route_navigation')) { } else if (meta!.keys.contains('maps_route_navigation')) {
final card_navigation = meta!['maps_route_navigation'] final card_navigation = meta!['maps_route_navigation']
['card_navigation'] as Map<String, dynamic>; ['card_navigation'] as Map<String, dynamic>?;
final start = card_navigation['start'] as String; if (card_navigation != null) {
final end = card_navigation['end'] as String; final start = card_navigation['start'] as String? ?? '';
final destination = card_navigation['destination'] as String; final end = card_navigation['end'] as String? ?? '';
final rawList = card_navigation['items'] as List; final destination = card_navigation['destination'] as String? ?? '';
final rawList = card_navigation['items'] as List? ?? [];
final list = rawList.map((e) => Map<String, dynamic>.from(e)).toList(); final list = rawList.map((e) => Map<String, dynamic>.from(e)).toList();
final rawNavigation = card_navigation['navigation']; final rawNavigation = card_navigation['navigation'];
final Map<String, dynamic>? navigation = final Map<String, dynamic>? navigation =
@ -289,6 +294,7 @@ class MessageBubble extends GetView<AgentController> {
modes: list, modes: list,
navigation: navigation); navigation: navigation);
} }
}
// if (message.contains('播放列表') || message.contains('音乐')) { // if (message.contains('播放列表') || message.contains('音乐')) {
// return _buildMusicContent(context); // return _buildMusicContent(context);

143
lib/modules/agent/widgets/sound_bar_widget.dart

@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
/// 声音柱动画组件
/// 用于显示语音输入时的音频波形效果
class SoundBarWidget extends StatefulWidget {
final double delay;
final bool isActive;
const SoundBarWidget({
Key? key,
required this.delay,
required this.isActive,
}) : super(key: key);
@override
State<SoundBarWidget> createState() => _SoundBarWidgetState();
}
class _SoundBarWidgetState extends State<SoundBarWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_animation = Tween<double>(begin: 3, end: 18).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
)..addListener(() {
setState(() {});
});
_startOrStopAnimation();
}
@override
void didUpdateWidget(SoundBarWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isActive != oldWidget.isActive) {
_startOrStopAnimation();
}
}
/// 开始或停止动画
void _startOrStopAnimation() {
if (widget.isActive) {
Future.delayed(Duration(milliseconds: (widget.delay * 1000).toInt()), () {
if (mounted && widget.isActive) {
_controller.repeat(reverse: true);
}
});
} else {
_controller.reset();
// 设置为小高度的静态状态
_controller.animateTo(0.1, duration: const Duration(milliseconds: 300));
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final primaryColor = Theme.of(context).primaryColor;
return Container(
width: 2.5,
height: widget.isActive ? _animation.value : 3,
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: BoxDecoration(
color: widget.isActive
? primaryColor.withOpacity(0.8)
: Colors.grey.withOpacity(0.4),
borderRadius: BorderRadius.circular(1.5),
),
);
}
}
/// 声音柱组合组件
/// 包含多个声音柱的组合显示
class SoundBarsWidget extends StatelessWidget {
final bool isActive;
final int barCount;
const SoundBarsWidget({
Key? key,
required this.isActive,
this.barCount = 10,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: List.generate(barCount, (index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 1),
child: SoundBarWidget(
delay: index * 0.1,
isActive: isActive,
),
);
}),
);
}
}
/// 带控制器集成的声音柱组件
/// 自动从AgentController获取状态
class AgentSoundBarsWidget extends StatelessWidget {
final int barCount;
const AgentSoundBarsWidget({
Key? key,
this.barCount = 10,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Obx(() {
final controller = Get.find<AgentController>();
return SoundBarsWidget(
isActive: controller.isListening.value,
barCount: barCount,
);
});
}
}

209
lib/modules/meeting/views/ai_bottom_sheet.dart

@ -1,7 +1,11 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../data/services/deapsound_ai_service.dart';
import '../controllers/meeting_details_controller.dart'; import '../controllers/meeting_details_controller.dart';
class AIBottomSheet extends StatefulWidget { class AIBottomSheet extends StatefulWidget {
@ -13,6 +17,14 @@ class AIBottomSheet extends StatefulWidget {
class _AIBottomSheetState extends State<AIBottomSheet> { class _AIBottomSheetState extends State<AIBottomSheet> {
final _controller = Get.find<MeetingDetailsController>(); final _controller = Get.find<MeetingDetailsController>();
final ScrollController _scrollController = ScrollController();
final _deapsoundAI = DeapsoundAIService();
StreamSubscription? _aiResponseSubscription;
final TextEditingController _texController = TextEditingController();
String _allText = '';
List _dataList = []; List _dataList = [];
@ -22,11 +34,38 @@ class _AIBottomSheetState extends State<AIBottomSheet> {
'生成关键指标', '生成关键指标',
]; ];
bool _isLoading = false;
@override
void initState() {
super.initState();
_getAllText();
}
void _getAllText() {
for (var element in _controller.textList) {
if (element['speaker'].isNotEmpty) {
_allText += '${element['speaker']}:';
}
_allText += '${element['content']}\n';
}
}
@override
void dispose() {
_texController.dispose();
_scrollController.dispose();
_aiResponseSubscription?.cancel();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
height: 1.sh - 50.h,
padding: EdgeInsets.symmetric(horizontal: 12.w), padding: EdgeInsets.symmetric(horizontal: 12.w),
constraints: BoxConstraints(
maxHeight: 1.sh - 50.h - MediaQuery.of(context).viewInsets.bottom,
),
child: Column( child: Column(
children: [ children: [
Container( Container(
@ -49,7 +88,7 @@ class _AIBottomSheetState extends State<AIBottomSheet> {
width: 26.w, width: 26.w,
height: 26.w, height: 26.w,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.grey[100],
borderRadius: BorderRadius.circular(26.r), borderRadius: BorderRadius.circular(26.r),
), ),
child: Icon( child: Icon(
@ -58,12 +97,26 @@ class _AIBottomSheetState extends State<AIBottomSheet> {
color: Colors.grey[400], color: Colors.grey[400],
), ),
), ),
) ),
], ],
), ),
), ),
Expanded( Expanded(
child: _dataList.isNotEmpty ? SizedBox() : _empty(), child: _dataList.isNotEmpty
? Align(
alignment: Alignment.topCenter,
child: ListView.builder(
controller: _scrollController,
itemCount: _dataList.length,
shrinkWrap: true,
reverse: true,
itemBuilder: (BuildContext context, int index) {
Map data = _dataList[index];
return _item(data);
},
),
)
: _empty(),
), ),
_inputBox(), _inputBox(),
], ],
@ -71,6 +124,38 @@ class _AIBottomSheetState extends State<AIBottomSheet> {
); );
} }
Widget _item(Map data) {
return Column(
children: [
Container(
padding: EdgeInsets.symmetric(vertical: 10.w),
alignment: Alignment.centerRight,
child: Container(
padding: EdgeInsets.all(10.w),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
data['user'],
style: TextStyle(
fontSize: 14.sp,
),
),
),
),
if (data['assistant'].isNotEmpty)
Container(
padding: EdgeInsets.symmetric(vertical: 10.w),
alignment: Alignment.centerLeft,
child: MarkdownBody(
data: data['assistant'],
),
),
],
);
}
Widget _empty() { Widget _empty() {
return Padding( return Padding(
padding: EdgeInsets.symmetric(vertical: 10.w), padding: EdgeInsets.symmetric(vertical: 10.w),
@ -106,7 +191,11 @@ class _AIBottomSheetState extends State<AIBottomSheet> {
), ),
...List.generate( ...List.generate(
_cruxList.length, _cruxList.length,
(int index) => Container( (int index) => GestureDetector(
onTap: () {
_sendMessage(_cruxList[index]);
},
child: Container(
width: double.infinity, width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w), padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w),
margin: EdgeInsets.symmetric(vertical: 5.w), margin: EdgeInsets.symmetric(vertical: 5.w),
@ -123,6 +212,7 @@ class _AIBottomSheetState extends State<AIBottomSheet> {
), ),
), ),
), ),
),
], ],
), ),
); );
@ -130,12 +220,115 @@ class _AIBottomSheetState extends State<AIBottomSheet> {
Widget _inputBox() { Widget _inputBox() {
return Container( return Container(
height: 40.w, height: 42.w,
padding: EdgeInsets.symmetric(horizontal: 10.w), padding: EdgeInsets.symmetric(horizontal: 10.w),
decoration: BoxDecoration(), margin: EdgeInsets.only(bottom: 10.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(42.r),
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
const Color(0xFFab47bc).withOpacity(0.1), // 薰衣草紫
const Color(0xFFec407a).withOpacity(0.2), // 品红
],
),
),
child: Row( child: Row(
children: [], children: [
5.horizontalSpace,
Expanded(
child: TextField(
controller: _texController,
style: TextStyle(
fontSize: 16.sp,
color: Colors.black87,
letterSpacing: 0,
height: 1.4,
),
decoration: InputDecoration(
hintText: '问 AI ...',
hintStyle: TextStyle(
fontSize: 16.sp,
color: Colors.grey[400],
letterSpacing: 0,
height: 1.4,
),
isDense: true,
fillColor: Colors.transparent,
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
onSubmitted: _sendMessage,
),
),
10.horizontalSpace,
GestureDetector(
onTap: () {
_sendMessage(_texController.text);
},
child: Container(
width: 26.w,
height: 26.w,
decoration: BoxDecoration(
color: _isLoading ? Colors.black87 : Colors.grey,
borderRadius: BorderRadius.circular(26.r),
),
child: Icon(
_isLoading ? Icons.pause : Icons.arrow_upward,
size: 18.w,
color: Colors.white,
),
),
),
],
), ),
); );
} }
void _sendMessage(String value) {
if (_isLoading || value.isEmpty) return;
_texController.clear();
setState(() {
_isLoading = true;
_dataList.insert(0, {
'user': value,
'assistant': '',
});
});
if (_dataList.length > 1) {
_scrollController.jumpTo(0);
}
final responseStream = _deapsoundAI.sendMessageStream(
messages: [
..._getUserMessage(),
{'role': 'user', 'content': value},
],
);
_aiResponseSubscription = responseStream.listen(
(chunk) {
setState(() {
_dataList.first['assistant'] += chunk;
});
},
onDone: () {
setState(() {
_isLoading = false;
});
},
);
}
List _getUserMessage() {
List<Map<String, String>> messages = [
{
'role': 'system',
'content': '你只能根据这段对话记录回答问题,禁止引入外部知识。对话内容:$_allText',
},
];
for (int i = _dataList.length - 1; i >= 0; i--) {
messages.add({'role': 'user', 'content': _dataList[i]['user']});
}
return messages;
}
} }

Loading…
Cancel
Save