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. 50
      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. 1105
      lib/modules/agent/views/agent_view.dart
  8. 78
      lib/modules/agent/views/message_bubble.dart
  9. 143
      lib/modules/agent/widgets/sound_bar_widget.dart
  10. 233
      lib/modules/meeting/views/ai_bottom_sheet.dart

1
.gitignore

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

50
lib/data/services/deapsound_ai_service.dart

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

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

@ -1,11 +1,11 @@
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
import '../controllers/report_controller.dart';
import '../controllers/message_action_controller.dart';
class AgentBinding extends Bindings {
@override
void dependencies() {
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 shouldShowExpandButton = false.obs; // 是否显示展开按钮
// 图片选择选项展开状态
final isImageOptionsExpanded = false.obs; // 是否展开图片选择选项
// 键盘状态
final isKeyboardVisible = false.obs; // 键盘是否可见
// 流事件订阅
StreamSubscription<AgentServiceEvent>? _eventSubscription;
@ -898,6 +904,58 @@ class AgentController extends GetxController {
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) {
// 确保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),
);
}
}

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

File diff suppressed because it is too large

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

@ -233,29 +233,33 @@ class MessageBubble extends GetView<AgentController> {
// 判断消息类型,显示不同的卡片
if (meta!.keys.contains('music_play')) {
final card_music =
meta!["music_play"]['card_music'] as Map<String, dynamic>;
final musicId = card_music!['id'];
final musicName = card_music!['name'] as String;
final musicUrl = card_music!['url'] as String;
final musicSinger = card_music!['sgener'] as String;
final musicImage = card_music!['image'] as String;
//音乐卡片
return buildMusicItem(
context, musicId, musicName, musicUrl, musicSinger, musicImage);
meta!["music_play"]['card_music'] as Map<String, dynamic>?;
if (card_music != null) {
final musicId = card_music['id'];
final musicName = card_music['name'] as String? ?? '未知歌曲';
final musicUrl = card_music['url'] as String? ?? '';
final musicSinger = card_music['sgener'] as String? ?? '未知歌手';
final musicImage = card_music['image'] as String? ?? '';
//音乐卡片
return buildMusicItem(
context, musicId, musicName, musicUrl, musicSinger, musicImage);
}
} else if (meta!.keys.contains('music_playlist')) {
final card_musiclist =
meta!["music_playlist"]['card_musiclist'] as Map<String, dynamic>;
final rawList = card_musiclist['musics'] as List;
final playlist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
//音乐播放列表
return buildMusicContent(context, playlist);
meta!["music_playlist"]['card_musiclist'] as Map<String, dynamic>?;
if (card_musiclist != null) {
final rawList = card_musiclist['musics'] as List? ?? [];
final playlist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
//音乐播放列表
return buildMusicContent(context, playlist);
}
} else if (meta!.keys.contains('maps_weather')) {
final city = meta!['maps_weather']['city'] as String;
final rawList = meta!['maps_weather']['forecasts'] as List;
final city = meta!['maps_weather']['city'] as String? ?? '未知城市';
final rawList = meta!['maps_weather']['forecasts'] as List? ?? [];
final daylist =
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(
context,
@ -264,30 +268,32 @@ class MessageBubble extends GetView<AgentController> {
daylist,
);
} 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 =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
// 旅游
return buildTravelContent(context, travellist);
} else if (meta!.keys.contains('maps_route_navigation')) {
final card_navigation = meta!['maps_route_navigation']
['card_navigation'] as Map<String, dynamic>;
final start = card_navigation['start'] as String;
final end = card_navigation['end'] as String;
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 rawNavigation = card_navigation['navigation'];
final Map<String, dynamic>? navigation =
rawNavigation is Map<String, dynamic> ? rawNavigation : null;
print("导航实时数据 ${rawNavigation}");
// 导航
return navigationCard(context,
start: start,
end: end,
destination: destination,
modes: list,
navigation: navigation);
['card_navigation'] as Map<String, dynamic>?;
if (card_navigation != null) {
final start = card_navigation['start'] as String? ?? '';
final end = card_navigation['end'] as String? ?? '';
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 rawNavigation = card_navigation['navigation'];
final Map<String, dynamic>? navigation =
rawNavigation is Map<String, dynamic> ? rawNavigation : null;
print("导航实时数据 ${rawNavigation}");
// 导航
return navigationCard(context,
start: start,
end: end,
destination: destination,
modes: list,
navigation: navigation);
}
}
// if (message.contains('播放列表') || message.contains('音乐')) {

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,
);
});
}
}

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

@ -1,7 +1,11 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../../data/services/deapsound_ai_service.dart';
import '../controllers/meeting_details_controller.dart';
class AIBottomSheet extends StatefulWidget {
@ -13,6 +17,14 @@ class AIBottomSheet extends StatefulWidget {
class _AIBottomSheetState extends State<AIBottomSheet> {
final _controller = Get.find<MeetingDetailsController>();
final ScrollController _scrollController = ScrollController();
final _deapsoundAI = DeapsoundAIService();
StreamSubscription? _aiResponseSubscription;
final TextEditingController _texController = TextEditingController();
String _allText = '';
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
Widget build(BuildContext context) {
return Container(
height: 1.sh - 50.h,
padding: EdgeInsets.symmetric(horizontal: 12.w),
constraints: BoxConstraints(
maxHeight: 1.sh - 50.h - MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
children: [
Container(
@ -49,7 +88,7 @@ class _AIBottomSheetState extends State<AIBottomSheet> {
width: 26.w,
height: 26.w,
decoration: BoxDecoration(
color: Colors.white,
color: Colors.grey[100],
borderRadius: BorderRadius.circular(26.r),
),
child: Icon(
@ -58,12 +97,26 @@ class _AIBottomSheetState extends State<AIBottomSheet> {
color: Colors.grey[400],
),
),
)
),
],
),
),
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(),
],
@ -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() {
return Padding(
padding: EdgeInsets.symmetric(vertical: 10.w),
@ -106,20 +191,25 @@ class _AIBottomSheetState extends State<AIBottomSheet> {
),
...List.generate(
_cruxList.length,
(int index) => Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(20.r),
border: Border.all(
color: Colors.grey[300]!,
(int index) => GestureDetector(
onTap: () {
_sendMessage(_cruxList[index]);
},
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(20.r),
border: Border.all(
color: Colors.grey[300]!,
),
),
child: Text(
_cruxList[index],
style: TextStyle(fontSize: 12.sp),
),
),
child: Text(
_cruxList[index],
style: TextStyle(fontSize: 12.sp),
),
),
),
@ -130,12 +220,115 @@ class _AIBottomSheetState extends State<AIBottomSheet> {
Widget _inputBox() {
return Container(
height: 40.w,
height: 42.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(
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