From 441a227c723ed8f250c44b6ccb4bb05a96d489bf Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Fri, 27 Jun 2025 14:47:02 +0800 Subject: [PATCH 1/3] =?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 6559b31a6fe788268c542b36dd787e2575e4abad Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Fri, 27 Jun 2025 16:21:33 +0800 Subject: [PATCH 2/3] =?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 75e8e06bd76b368f69ec741758fd8351c67ecfbb Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Fri, 27 Jun 2025 17:27:36 +0800 Subject: [PATCH 3/3] =?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; + } }