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 9243acad8..3b847c465 100644 --- a/lib/modules/meeting/views/ai_bottom_sheet.dart +++ b/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 { final _controller = Get.find(); + 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 { '生成关键指标', ]; + 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 { 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 { 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 { ); } + 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 { ), ...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 { 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> messages = [ + { + 'role': 'system', + 'content': '你只能根据这段对话记录回答问题,禁止引入外部知识。对话内容:$_allText', + }, + ]; + for (int i = _dataList.length - 1; i >= 0; i--) { + messages.add({'role': 'user', 'content': _dataList[i]['user']}); + } + return messages; + } }