From a056214fcffe321edef7cc966c316cb391086e02 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Fri, 27 Jun 2025 14:47:02 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E4=BF=AE=E6=94=B9askai?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting/views/ai_bottom_sheet.dart | 218 ++++++++++++++++-- 1 file changed, 198 insertions(+), 20 deletions(-) diff --git a/lib/modules/meeting/views/ai_bottom_sheet.dart b/lib/modules/meeting/views/ai_bottom_sheet.dart index 9243acad8..db5f964df 100644 --- a/lib/modules/meeting/views/ai_bottom_sheet.dart +++ b/lib/modules/meeting/views/ai_bottom_sheet.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; @@ -14,6 +15,8 @@ class AIBottomSheet extends StatefulWidget { class _AIBottomSheetState extends State { final _controller = Get.find(); + final TextEditingController _texController = TextEditingController(); + List _dataList = []; final List _cruxList = [ @@ -22,11 +25,21 @@ class _AIBottomSheetState extends State { '生成关键指标', ]; + bool _isLoading = false; + + @override + void dispose() { + _texController.dispose(); + 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 +62,7 @@ class _AIBottomSheetState extends State { width: 26.w, height: 26.w, decoration: BoxDecoration( - color: Colors.white, + color: Colors.grey[100], borderRadius: BorderRadius.circular(26.r), ), child: Icon( @@ -58,12 +71,25 @@ class _AIBottomSheetState extends State { color: Colors.grey[400], ), ), - ) + ), ], ), ), Expanded( - child: _dataList.isNotEmpty ? SizedBox() : _empty(), + child: _dataList.isNotEmpty + ? Align( + alignment: Alignment.topCenter, + child: ListView.builder( + itemCount: _dataList.length, + shrinkWrap: true, + reverse: true, + itemBuilder: (BuildContext context, int index) { + Map data = _dataList[index]; + return _item(data, index == 0); + }, + ), + ) + : _empty(), ), _inputBox(), ], @@ -71,6 +97,50 @@ class _AIBottomSheetState extends State { ); } + Widget _item(Map data, bool isBorder) { + return Container( + padding: EdgeInsets.symmetric(vertical: 20.w), + decoration: BoxDecoration( + border: isBorder + ? null + : Border( + bottom: BorderSide( + width: 1, + color: Colors.grey[400]!, + ), + ), + ), + child: Column( + children: [ + Align( + 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: 20.w), + alignment: Alignment.centerLeft, + child: MarkdownBody( + data: data['assistant'], + ), + ), + ], + ), + ); + } + Widget _empty() { return Padding( padding: EdgeInsets.symmetric(vertical: 10.w), @@ -106,20 +176,25 @@ class _AIBottomSheetState extends State { ), ...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 +205,115 @@ class _AIBottomSheetState extends State { 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: Colors.grey, + borderRadius: BorderRadius.circular(26.r), + ), + child: Icon( + Icons.arrow_upward, + size: 18.w, + color: Colors.white, + ), + ), + ), + ], ), ); } + + void _sendMessage(String value) { + if (_isLoading || value.isEmpty) return; + setState(() { + _isLoading = true; + }); + _dataList.insert(0, { + 'user': value, + 'assistant': ''' +# 角色 +你是一名卓越的会议记录专家,拥有超强的信息归纳与细分能力。不仅能高效处理各类通用场景会议,精准提炼会议核心内容,为后续思维导图生成奠定坚实基础,还能将整理好的会议记录巧妙转化为清晰直观的思维导图。 + +## 技能 +### 技能 1: 通用会议内容深度细分 +1. 深入全面剖析给定的通用场景会议内容,精准区分不同主题板块,如会议目标、讨论事项、决策结果、行动安排、问题与挑战等。 +2. 对每个主题板块进行细致分解,形成具体要点与细节。若涉及人员,人员名称保留[],清晰明确其角色与贡献;涉及流程,梳理出关键步骤与节点。 +3. 以层次分明、逻辑紧密的方式整理细分内容,使其契合思维导图的结构特点。 +===回复示例=== +- **会议目标**: + - 目标 1:[详细且全面阐述目标 1] + - 目标 2:[如有多个目标,依次详细罗列] +- **讨论事项**: + - 事项 1: + - 观点 1:[详细说明提出观点 1 的人员及具体内容] + - 观点 2:[详细阐述不同观点及相关人员] + - 事项 2:[按此格式详细罗列其他讨论事项] +- **决策结果**: + - 决策 1:[具体且完整表述决策内容] + - 决策 2:[详细列出其他决策] +- **行动安排**: + - 任务 1: + - 负责人:[具体姓名] + - 时间节点:[精确到具体时间] + - 任务内容:[详细描述任务具体内容] + - 任务 2:[依此格式详细罗列其他任务] +- **问题与挑战**: + - 问题 1:[具体清晰描述问题] + - 挑战 1:[详细说明面临的挑战及相关说明] +===示例结束=== +''', + }); + _texController.clear(); + setState(() { + _isLoading = false; + }); + } } From 8cb0688a88815705aecd9097b0c5373b5ad2f063 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Fri, 27 Jun 2025 16:21:33 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E4=BF=AE=E6=94=B9askai?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/deapsound_ai_service.dart | 50 ++----- .../meeting/views/ai_bottom_sheet.dart | 140 ++++++++---------- 2 files changed, 76 insertions(+), 114 deletions(-) diff --git a/lib/data/services/deapsound_ai_service.dart b/lib/data/services/deapsound_ai_service.dart index 4701bf674..da7181f4f 100644 --- a/lib/data/services/deapsound_ai_service.dart +++ b/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(); } diff --git a/lib/modules/meeting/views/ai_bottom_sheet.dart b/lib/modules/meeting/views/ai_bottom_sheet.dart index db5f964df..760274ed6 100644 --- a/lib/modules/meeting/views/ai_bottom_sheet.dart +++ b/lib/modules/meeting/views/ai_bottom_sheet.dart @@ -1,8 +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 { @@ -14,6 +17,10 @@ class AIBottomSheet extends StatefulWidget { class _AIBottomSheetState extends State { final _controller = Get.find(); + final ScrollController _scrollController = ScrollController(); + + final _deapsoundAI = DeapsoundAIService(); + StreamSubscription? _aiResponseSubscription; final TextEditingController _texController = TextEditingController(); @@ -30,6 +37,8 @@ class _AIBottomSheetState extends State { @override void dispose() { _texController.dispose(); + _scrollController.dispose(); + _aiResponseSubscription?.cancel(); super.dispose(); } @@ -80,12 +89,13 @@ class _AIBottomSheetState extends State { ? 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, index == 0); + return _item(data); }, ), ) @@ -97,47 +107,35 @@ class _AIBottomSheetState extends State { ); } - Widget _item(Map data, bool isBorder) { - return Container( - padding: EdgeInsets.symmetric(vertical: 20.w), - decoration: BoxDecoration( - border: isBorder - ? null - : Border( - bottom: BorderSide( - width: 1, - color: Colors.grey[400]!, - ), - ), - ), - child: Column( - children: [ - Align( - 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, - ), + 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: 20.w), - alignment: Alignment.centerLeft, - child: MarkdownBody( - data: data['assistant'], - ), + ), + if (data['assistant'].isNotEmpty) + Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + alignment: Alignment.centerLeft, + child: MarkdownBody( + data: data['assistant'], ), - ], - ), + ), + ], ); } @@ -256,11 +254,11 @@ class _AIBottomSheetState extends State { width: 26.w, height: 26.w, decoration: BoxDecoration( - color: Colors.grey, + color: _isLoading ? Colors.black87 : Colors.grey, borderRadius: BorderRadius.circular(26.r), ), child: Icon( - Icons.arrow_upward, + _isLoading ? Icons.pause : Icons.arrow_upward, size: 18.w, color: Colors.white, ), @@ -273,47 +271,31 @@ class _AIBottomSheetState extends State { void _sendMessage(String value) { if (_isLoading || value.isEmpty) return; - setState(() { - _isLoading = true; - }); - _dataList.insert(0, { - 'user': value, - 'assistant': ''' -# 角色 -你是一名卓越的会议记录专家,拥有超强的信息归纳与细分能力。不仅能高效处理各类通用场景会议,精准提炼会议核心内容,为后续思维导图生成奠定坚实基础,还能将整理好的会议记录巧妙转化为清晰直观的思维导图。 - -## 技能 -### 技能 1: 通用会议内容深度细分 -1. 深入全面剖析给定的通用场景会议内容,精准区分不同主题板块,如会议目标、讨论事项、决策结果、行动安排、问题与挑战等。 -2. 对每个主题板块进行细致分解,形成具体要点与细节。若涉及人员,人员名称保留[],清晰明确其角色与贡献;涉及流程,梳理出关键步骤与节点。 -3. 以层次分明、逻辑紧密的方式整理细分内容,使其契合思维导图的结构特点。 -===回复示例=== -- **会议目标**: - - 目标 1:[详细且全面阐述目标 1] - - 目标 2:[如有多个目标,依次详细罗列] -- **讨论事项**: - - 事项 1: - - 观点 1:[详细说明提出观点 1 的人员及具体内容] - - 观点 2:[详细阐述不同观点及相关人员] - - 事项 2:[按此格式详细罗列其他讨论事项] -- **决策结果**: - - 决策 1:[具体且完整表述决策内容] - - 决策 2:[详细列出其他决策] -- **行动安排**: - - 任务 1: - - 负责人:[具体姓名] - - 时间节点:[精确到具体时间] - - 任务内容:[详细描述任务具体内容] - - 任务 2:[依此格式详细罗列其他任务] -- **问题与挑战**: - - 问题 1:[具体清晰描述问题] - - 挑战 1:[详细说明面临的挑战及相关说明] -===示例结束=== -''', - }); _texController.clear(); setState(() { - _isLoading = false; + _isLoading = true; + _dataList.insert(0, { + 'user': value, + 'assistant': '', + }); }); + _scrollController.jumpTo(0); + final responseStream = _deapsoundAI.sendMessageStream( + messages: [ + {'role': 'user', 'content': value}, + ], + ); + _aiResponseSubscription = responseStream.listen( + (chunk) { + setState(() { + _dataList.first['assistant'] += chunk; + }); + }, + onDone: () { + setState(() { + _isLoading = false; + }); + }, + ); } } From f7025b9a446c4f784c4ec44013e99d347af936d7 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 27 Jun 2025 16:46:06 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat(agent):=20=E6=B7=BB=E5=8A=A0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E9=80=89=E9=A1=B9=E5=B1=95=E5=BC=80=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=92=8C=E9=94=AE=E7=9B=98=E5=8F=AF=E8=A7=81=E6=80=A7=E6=8E=A7?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增图片选择选项的展开/收缩功能,并添加键盘可见性状态管理 当键盘显示时自动收缩图片选项,避免界面重叠 --- .gitignore | 1 + .../agent/controllers/agent_controller.dart | 58 ++ lib/modules/agent/views/agent_view.dart | 753 +++++++++++------- 3 files changed, 509 insertions(+), 303 deletions(-) diff --git a/.gitignore b/.gitignore index b4a242c24..63e1880cc 100644 --- a/.gitignore +++ b/.gitignore @@ -222,3 +222,4 @@ fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output fastlane/readme.md +.trae/rules/project_rules.md diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index c8729d062..3648b0639 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/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? _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 diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index e22175383..a754a1cca 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -11,12 +11,45 @@ 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); + @override + State createState() => _AgentViewState(); +} + +class _AgentViewState extends State with WidgetsBindingObserver { + late AgentController controller; + double _keyboardHeight = 0; + + @override + void initState() { + super.initState(); + controller = Get.find(); + 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 Widget build(BuildContext context) { - final controller = Get.find(); final theme = Theme.of(context); final primaryColor = theme.primaryColor; final backgroundColor = theme.scaffoldBackgroundColor; @@ -288,333 +321,391 @@ class AgentView extends StatelessWidget { } }), - // 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐 - IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, // 所有元素底部对齐 - children: [ - // 键盘/语音切换按钮 - Obx(() { - final isTextMode = controller.isTextInputMode.value; - return GestureDetector( - onTap: () { - controller.toggleInputMode(); - }, - child: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: BorderRadius.circular(22), - ), - child: Icon( - isTextMode ? Icons.mic_none : Icons.keyboard, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 22, - ), - ), - ); - }), - - const SizedBox(width: 8), - - // 文本输入框或语音波形图 - Expanded( - child: Obx(() { - final isTextMode = controller.isTextInputMode.value; - - if (isTextMode) { - // 文本输入模式 - 限制最大高度并只向上扩展 - return Container( - constraints: const BoxConstraints( - minHeight: 44, // 最小高度 - maxHeight: 120, // 最大高度限制,防止挤压按钮 - ), - padding: - const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: BorderRadius.circular(22), - border: Border.all( - color: isDarkMode - ? Colors.grey[ - 800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800] - : Colors.grey.withOpacity(0.1), - width: 1.0, - ), - ), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.end, // 对齐到底部,实现向上扩展 - children: [ - Expanded( - child: Obx(() { - // 图片模式下使用更高的输入框 - final isImageActive = - controller.isImageInputActive.value; - - return TextField( - controller: controller.textController, - maxLines: - isImageActive ? 3 : 5, // 设置合理的最大行数 - minLines: isImageActive ? 2 : 1, - keyboardType: - TextInputType.multiline, // 启用多行键盘 - textInputAction: - TextInputAction.newline, // 支持回车换行 - style: TextStyle( - fontSize: 15, - color: isDarkMode - ? Colors.white - : Colors.grey[800], - ), - decoration: InputDecoration( - hintText: isImageActive - ? '添加对图片的问题或描述...' - : '输入消息...', - hintStyle: TextStyle( - color: Colors.grey[500], - fontSize: 15, - ), - filled: false, - border: InputBorder.none, - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: isDarkMode - ? Colors.grey[800]! - : Colors.grey[200]!, - width: 1.0, - ), - ), - contentPadding: EdgeInsets.symmetric( - vertical: isImageActive - ? 12 - : 8), // 调整垂直内边距 - ), - onSubmitted: (_) => - controller.sendMessage(), - ); - }), - ), - ], - ), - ); - } else { - // 语音输入模式 - 始终显示波形图 + // 输入控件区域 - 包含输入行和展开的选项 + Column( + children: [ + // 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐 + IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, // 所有元素底部对齐 + children: [ + // 键盘/语音切换按钮 + Obx(() { + final isTextMode = controller.isTextInputMode.value; return GestureDetector( onTap: () { - if (controller.isListening.value) { - controller.stopVoiceInput(); - } else { - controller.startVoiceInput(); - } + controller.toggleInputMode(); }, child: Container( - height: 50, - padding: - const EdgeInsets.symmetric(horizontal: 16), + width: 44, + height: 44, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], borderRadius: BorderRadius.circular(22), - border: Border.all( - color: isDarkMode - ? Colors.grey[800]! - : Colors.grey.withOpacity(0.1), - width: 1.0, - ), ), - 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 - ? primaryColor - : Colors.grey[500], - ), - const SizedBox(width: 4), - Text( - '正在聆听', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: isListening - ? primaryColor - : Colors.grey[500], - ), - ), - ], - ); - }), - ], - ), - ), - ], + child: Icon( + isTextMode ? Icons.mic_none : Icons.keyboard, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 22, ), ), ); - } - }), - ), + }), - const SizedBox(width: 8), + const SizedBox(width: 8), - // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 - Obx(() { - final hasText = controller.hasTextInput.value; - final hasImage = controller.isImageInputActive.value; - final shouldShowSend = hasText || hasImage; - final shouldShowExpand = - controller.shouldShowExpandButton.value; + // 文本输入框或语音波形图 + Expanded( + child: Obx(() { + final isTextMode = + controller.isTextInputMode.value; - if (shouldShowSend) { - // 显示发送按钮(可能还有展开按钮) - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 展开按钮(在发送按钮上方) - if (shouldShowExpand && hasText) - Padding( - padding: const EdgeInsets.only(bottom: 40), - child: GestureDetector( - onTap: () { - // 打开底部弹出编辑模式 - controller.openFullScreenEdit(); - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => - const FullScreenEditor(), - ).then((_) { - // 弹出页关闭时的回调 - controller.closeFullScreenEdit(); - }); - }, - child: Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[700] - : Colors.grey[300], - borderRadius: BorderRadius.circular(16), - ), - child: Icon( - Icons.expand_less, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 20, - ), - ), + if (isTextMode) { + // 文本输入模式 - 限制最大高度并只向上扩展 + return Container( + constraints: const BoxConstraints( + minHeight: 44, // 最小高度 + maxHeight: 120, // 最大高度限制,防止挤压按钮 ), - ), - // 发送按钮 - GestureDetector( - onTap: () => controller.sendMessage(), - child: Container( - width: 44, - height: 44, + padding: const EdgeInsets.symmetric( + horizontal: 16), decoration: BoxDecoration( - color: primaryColor, + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], borderRadius: BorderRadius.circular(22), + border: Border.all( + color: isDarkMode + ? Colors.grey[ + 800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800] + : Colors.grey.withOpacity(0.1), + width: 1.0, + ), ), - child: const Icon( - Icons.send, - color: Colors.white, - size: 22, - ), - ), - ), - ], - ); - } else { - // 显示图片选择按钮 - return Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: BorderRadius.circular(22), - ), - child: PopupMenuButton( - icon: Icon( - Icons.add_photo_alternate_outlined, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - 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( - value: 'camera', child: Row( + crossAxisAlignment: + CrossAxisAlignment.end, // 对齐到底部,实现向上扩展 children: [ - Icon(Icons.camera_alt, - color: primaryColor, size: 20), - const SizedBox(width: 8), - const Text('拍照', - style: TextStyle(fontSize: 14)), + Expanded( + child: Obx(() { + // 图片模式下使用更高的输入框 + final isImageActive = controller + .isImageInputActive.value; + + return TextField( + controller: + controller.textController, + maxLines: isImageActive + ? 3 + : 5, // 设置合理的最大行数 + minLines: isImageActive ? 2 : 1, + keyboardType: TextInputType + .multiline, // 启用多行键盘 + textInputAction: TextInputAction + .newline, // 支持回车换行 + onTap: () { + // 当点击输入框时,如果图片选项已展开,则收缩 + if (controller + .isImageOptionsExpanded + .value) { + controller + .collapseImageOptions(); + } + }, + style: TextStyle( + fontSize: 15, + color: isDarkMode + ? Colors.white + : Colors.grey[800], + ), + decoration: InputDecoration( + hintText: isImageActive + ? '添加对图片的问题或描述...' + : '输入消息...', + hintStyle: TextStyle( + color: Colors.grey[500], + fontSize: 15, + ), + filled: false, + border: InputBorder.none, + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: isDarkMode + ? Colors.grey[800]! + : Colors.grey[200]!, + width: 1.0, + ), + ), + contentPadding: + EdgeInsets.symmetric( + vertical: isImageActive + ? 12 + : 8), // 调整垂直内边距 + ), + onSubmitted: (_) => + controller.sendMessage(), + ); + }), + ), ], ), - ), - PopupMenuItem( - value: 'gallery', - child: Row( - children: [ - Icon(Icons.photo_library, - color: primaryColor, size: 20), - const SizedBox(width: 8), - const Text('从相册选择', - style: TextStyle(fontSize: 14)), - ], + ); + } else { + // 语音输入模式 - 始终显示波形图 + return GestureDetector( + onTap: () { + if (controller.isListening.value) { + controller.stopVoiceInput(); + } else { + controller.startVoiceInput(); + } + }, + child: Container( + height: 50, + padding: const EdgeInsets.symmetric( + horizontal: 16), + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: isDarkMode + ? Colors.grey[800]! + : Colors.grey.withOpacity(0.1), + width: 1.0, + ), + ), + 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 + ? primaryColor + : Colors.grey[500], + ), + const SizedBox(width: 4), + Text( + '正在聆听', + style: TextStyle( + fontSize: 13, + fontWeight: + FontWeight.w500, + color: isListening + ? primaryColor + : Colors.grey[500], + ), + ), + ], + ); + }), + ], + ), + ), + ], + ), ), + ); + } + }), + ), + + const SizedBox(width: 8), + + // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 + Obx(() { + final hasText = controller.hasTextInput.value; + final hasImage = + controller.isImageInputActive.value; + final shouldShowSend = hasText || hasImage; + final shouldShowExpand = + controller.shouldShowExpandButton.value; + + if (shouldShowSend) { + // 显示发送按钮(可能还有展开按钮) + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 展开按钮(在发送按钮上方) + if (shouldShowExpand && hasText) + Padding( + padding: + const EdgeInsets.only(bottom: 40), + child: GestureDetector( + onTap: () { + // 打开底部弹出编辑模式 + controller.openFullScreenEdit(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => + const FullScreenEditor(), + ).then((_) { + // 弹出页关闭时的回调 + controller.closeFullScreenEdit(); + }); + }, + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[700] + : Colors.grey[300], + borderRadius: + BorderRadius.circular(16), + ), + child: Icon( + Icons.expand_less, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 20, + ), + ), + ), + ), + // 发送按钮 + GestureDetector( + onTap: () => controller.sendMessage(), + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: primaryColor, + borderRadius: BorderRadius.circular(22), + ), + child: const Icon( + Icons.send, + color: Colors.white, + size: 22, + ), + ), + ), + ], + ); + } else { + // 显示'+'选择按钮 + return Obx(() { + final isExpanded = + controller.isImageOptionsExpanded.value; + return GestureDetector( + onTap: () { + controller.toggleImageOptions(); + }, + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], + borderRadius: BorderRadius.circular(22), + ), + child: Icon( + isExpanded ? Icons.close : Icons.add, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 22, + ), + ), + ); + }); + } + }), + ], + ), + ), + + // 展开的'+'选择选项 + 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( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 改为均匀分布 + children: [ + // 相机按钮 + _buildPlusOptionButton( + context: context, + icon: Icons.camera_alt, + 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(), + ); + }), + ], ), ], ), @@ -792,6 +883,62 @@ class AgentView extends StatelessWidget { )), ); } + + // 构建"+"号展开选项按钮 + Widget _buildPlusOptionButton({ + required BuildContext context, + required IconData icon, + required String label, + required VoidCallback onTap, + required bool isDarkMode, + }) { + final primaryColor = Theme.of(context).primaryColor; + + return GestureDetector( + onTap: onTap, + child: SizedBox( + // 调整为一行四个按钮的宽度,考虑屏幕宽度和间距 + width: (1.sw - 100.w) / 4, // 屏幕宽度减去左右边距,除以4个按钮 + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 正方形图标容器 - 增大尺寸 + Container( + width: 50.w, // 调整为44.w以适应布局 + height: 50.w, // 调整为44.w以适应布局 + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.grey[100], + borderRadius: BorderRadius.circular(12.r), // 调整为10.r + border: Border.all( + color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!, + width: 1, + ), + ), + child: Icon( + icon, + color: primaryColor, + size: 20.sp, // 调整为18.sp + ), + ), + SizedBox(height: 4.h), // 调整为3.h + // 文字标签 + Text( + label, + style: TextStyle( + fontSize: 10.sp, // 调整为9.sp + color: isDarkMode ? Colors.grey[300] : Colors.grey[700], + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + } } class _ReportForm extends StatelessWidget { From 36aa3649db42e3908ed784c99aed2851ebf44f47 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Fri, 27 Jun 2025 17:27:36 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E4=BC=98=E5=8C=96askai?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting/views/ai_bottom_sheet.dart | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/modules/meeting/views/ai_bottom_sheet.dart b/lib/modules/meeting/views/ai_bottom_sheet.dart index 760274ed6..3b847c465 100644 --- a/lib/modules/meeting/views/ai_bottom_sheet.dart +++ b/lib/modules/meeting/views/ai_bottom_sheet.dart @@ -24,6 +24,8 @@ class _AIBottomSheetState extends State { final TextEditingController _texController = TextEditingController(); + String _allText = ''; + List _dataList = []; final List _cruxList = [ @@ -34,6 +36,21 @@ class _AIBottomSheetState extends State { 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(); @@ -279,9 +296,12 @@ class _AIBottomSheetState extends State { 'assistant': '', }); }); - _scrollController.jumpTo(0); + if (_dataList.length > 1) { + _scrollController.jumpTo(0); + } final responseStream = _deapsoundAI.sendMessageStream( messages: [ + ..._getUserMessage(), {'role': 'user', 'content': value}, ], ); @@ -298,4 +318,17 @@ class _AIBottomSheetState extends State { }, ); } + + List _getUserMessage() { + List> messages = [ + { + 'role': 'system', + 'content': '你只能根据这段对话记录回答问题,禁止引入外部知识。对话内容:$_allText', + }, + ]; + for (int i = _dataList.length - 1; i >= 0; i--) { + messages.add({'role': 'user', 'content': _dataList[i]['user']}); + } + return messages; + } } From 0d13612e4801ce0855d34f04855eca03f56ab16b Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 27 Jun 2025 17:39:38 +0800 Subject: [PATCH 5/7] =?UTF-8?q?refactor(agent):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=93=8D=E4=BD=9C=E9=80=BB=E8=BE=91=E8=87=B3?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E6=8E=A7=E5=88=B6=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原本分散在AgentView中的消息操作逻辑(复制、举报等)提取到独立的MessageActionController中 删除不再使用的ReportController 更新相关绑定和视图引用 --- lib/modules/agent/bindings/agent_binding.dart | 4 +- .../message_action_controller.dart | 402 ++++++++++++++++++ .../agent/controllers/report_controller.dart | 80 ---- lib/modules/agent/views/agent_view.dart | 274 +----------- 4 files changed, 409 insertions(+), 351 deletions(-) create mode 100644 lib/modules/agent/controllers/message_action_controller.dart delete mode 100644 lib/modules/agent/controllers/report_controller.dart diff --git a/lib/modules/agent/bindings/agent_binding.dart b/lib/modules/agent/bindings/agent_binding.dart index ebb69cab2..4ae587113 100644 --- a/lib/modules/agent/bindings/agent_binding.dart +++ b/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()); - Get.lazyPut(() => ReportController()); + Get.lazyPut(() => MessageActionController()); } } diff --git a/lib/modules/agent/controllers/message_action_controller.dart b/lib/modules/agent/controllers/message_action_controller.dart new file mode 100644 index 000000000..00efd5963 --- /dev/null +++ b/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(); + + // 选择的举报原因 + List 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 _buildMenuItems( + BuildContext context, + bool isDarkMode, + Message message, + ) { + final List 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 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(); + + _ReportForm({required this.isDarkMode}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(16.w), + child: SingleChildScrollView( + child: GetBuilder( + 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( + 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, + ), + ), + ), + ); + } +} diff --git a/lib/modules/agent/controllers/report_controller.dart b/lib/modules/agent/controllers/report_controller.dart deleted file mode 100644 index 816d8277d..000000000 --- a/lib/modules/agent/controllers/report_controller.dart +++ /dev/null @@ -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(); - - // 选择的举报原因 - List 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 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), - ); - } -} diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index a754a1cca..5577c6502 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -2,14 +2,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/agent_controller.dart'; -import '../controllers/report_controller.dart'; +import '../controllers/message_action_controller.dart'; import 'message_bubble.dart'; import '../widgets/full_screen_editor.dart'; -import 'package:flutter/services.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 StatefulWidget { const AgentView({Key? key}) : super(key: key); @@ -20,12 +16,14 @@ class AgentView extends StatefulWidget { class _AgentViewState extends State with WidgetsBindingObserver { late AgentController controller; + late MessageActionController messageActionController; double _keyboardHeight = 0; @override void initState() { super.initState(); controller = Get.find(); + messageActionController = Get.find(); WidgetsBinding.instance.addObserver(this); } @@ -205,7 +203,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { padding: const EdgeInsets.only(bottom: 8), child: GestureDetector( onLongPressStart: (LongPressStartDetails details) { - _onLongPressMessageContent( + messageActionController.onLongPressMessage( context, isDarkMode, details, message); }, child: MessageBubble( @@ -715,130 +713,11 @@ class _AgentViewState extends State with WidgetsBindingObserver { ); } - 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.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({ @@ -941,150 +820,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { } } -class _ReportForm extends StatelessWidget { - final bool isDarkMode; - final ReportController reportController = Get.find(); - - _ReportForm({required this.isDarkMode}); - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.all(16.w), - child: SingleChildScrollView( - child: GetBuilder( - 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( - 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: 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), - ], - ), - ), - ), - ); - } - - Widget _buildReportChip( - 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( - label, - style: TextStyle( - color: isSelected - ? Colors.white - : (isDarkMode ? Colors.white : Colors.black87), - fontSize: 14.sp, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - ), - ), - ), - ); - } -} // 声音柱动画组件 class _SoundBar extends StatefulWidget { From 951e457883d54df1f50d9e755c856a55f656d399 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 27 Jun 2025 18:48:48 +0800 Subject: [PATCH 6/7] =?UTF-8?q?feat(widgets):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=A3=B0=E9=9F=B3=E6=9F=B1=E5=8A=A8=E7=94=BB=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E5=B9=B6=E6=9B=BF=E6=8D=A2=E5=8E=9F=E6=9C=89=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将声音柱动画逻辑提取为独立组件SoundBarWidget,并添加组合组件SoundBarsWidget和集成控制器的AgentSoundBarsWidget 移除agent_view中原有的_SoundBar实现,使用新组件替代 --- lib/modules/agent/views/agent_view.dart | 100 +----------- .../agent/widgets/sound_bar_widget.dart | 143 ++++++++++++++++++ 2 files changed, 146 insertions(+), 97 deletions(-) create mode 100644 lib/modules/agent/widgets/sound_bar_widget.dart diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 5577c6502..a1f8361a2 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -5,6 +5,7 @@ import '../controllers/agent_controller.dart'; import '../controllers/message_action_controller.dart'; import 'message_bubble.dart'; import '../widgets/full_screen_editor.dart'; +import '../widgets/sound_bar_widget.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; class AgentView extends StatefulWidget { @@ -486,9 +487,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { mainAxisAlignment: MainAxisAlignment.start, children: [ - ...List.generate(10, (index) { - return _buildSoundBar(index); - }), + const AgentSoundBarsWidget(), const Spacer(), // "正在聆听"文字 Obx(() { @@ -752,16 +751,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { ); } - // 构建声音柱 - Widget _buildSoundBar(int index) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 1), - child: Obx(() => _SoundBar( - delay: index * 0.1, - isActive: Get.find().isListening.value, - )), - ); - } + // 构建"+"号展开选项按钮 Widget _buildPlusOptionButton({ @@ -822,88 +812,4 @@ class _AgentViewState extends State with WidgetsBindingObserver { -// 声音柱动画组件 -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 _animation; - - @override - void initState() { - super.initState(); - - _controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 500), - ); - - _animation = Tween(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), - ), - ); - } -} diff --git a/lib/modules/agent/widgets/sound_bar_widget.dart b/lib/modules/agent/widgets/sound_bar_widget.dart new file mode 100644 index 000000000..e5c483b36 --- /dev/null +++ b/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 createState() => _SoundBarWidgetState(); +} + +class _SoundBarWidgetState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 500), + ); + + _animation = Tween(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(); + return SoundBarsWidget( + isActive: controller.isListening.value, + barCount: barCount, + ); + }); + } +} From 4ff77a550649b04532f632906c3b10d73711331d Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 27 Jun 2025 20:03:12 +0800 Subject: [PATCH 7/7] =?UTF-8?q?fix(agent):=20=E4=BF=AE=E5=A4=8D=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=B0=94=E6=B3=A1=E5=8D=A1=E7=89=87=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E4=B8=BAnull=E6=97=B6=E7=9A=84=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加对卡片数据可能为null的检查,并为各类型卡片提供默认值以避免空指针异常 --- lib/modules/agent/views/message_bubble.dart | 78 +++++++++++---------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index 819c822b9..0c4e2d782 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -233,29 +233,33 @@ class MessageBubble extends GetView { // 判断消息类型,显示不同的卡片 if (meta!.keys.contains('music_play')) { final card_music = - meta!["music_play"]['card_music'] as Map; - 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?; + 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; - final rawList = card_musiclist['musics'] as List; - final playlist = - rawList.map((e) => Map.from(e)).toList(); - //音乐播放列表 - return buildMusicContent(context, playlist); + meta!["music_playlist"]['card_musiclist'] as Map?; + if (card_musiclist != null) { + final rawList = card_musiclist['musics'] as List? ?? []; + final playlist = + rawList.map((e) => Map.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.from(e)).toList(); - final now = daylist[0] as Map; + final now = daylist.isNotEmpty ? daylist[0] : {}; //天气 return buildWeatherContent( context, @@ -264,30 +268,32 @@ class MessageBubble extends GetView { 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.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; - 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.from(e)).toList(); - final rawNavigation = card_navigation['navigation']; - final Map? navigation = - rawNavigation is Map ? rawNavigation : null; - print("导航实时数据 ${rawNavigation}"); - // 导航 - return navigationCard(context, - start: start, - end: end, - destination: destination, - modes: list, - navigation: navigation); + ['card_navigation'] as Map?; + 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.from(e)).toList(); + final rawNavigation = card_navigation['navigation']; + final Map? navigation = + rawNavigation is Map ? rawNavigation : null; + print("导航实时数据 ${rawNavigation}"); + // 导航 + return navigationCard(context, + start: start, + end: end, + destination: destination, + modes: list, + navigation: navigation); + } } // if (message.contains('播放列表') || message.contains('音乐')) {