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