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 { const AIBottomSheet({super.key}); @override State createState() => _AIBottomSheetState(); } 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 = []; final List _cruxList = [ '添加代办事项', '提取结论', '生成关键指标', ]; 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( padding: EdgeInsets.symmetric(horizontal: 12.w), constraints: BoxConstraints( maxHeight: 1.sh - 50.h - MediaQuery.of(context).viewInsets.bottom, ), child: Column( children: [ Container( padding: EdgeInsets.symmetric(vertical: 10.w), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ 26.horizontalSpace, Text( 'Ask AI', style: TextStyle( fontSize: 16.sp, color: Colors.black87, fontWeight: FontWeight.bold, ), ), GestureDetector( onTap: () => Get.back(), child: Container( width: 26.w, height: 26.w, decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(26.r), ), child: Icon( Icons.close, size: 18.w, color: Colors.grey[400], ), ), ), ], ), ), Expanded( 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(), ], ), ); } 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), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '今天我能帮您做点什么呢?', style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.bold, ), ), Padding( padding: EdgeInsets.only(top: 10.w, bottom: 30.w), child: Row( children: [ Icon( Icons.audio_file_outlined, size: 14.w, color: Colors.grey[500], ), 2.horizontalSpace, Text( _controller.meetingData.value.title, style: TextStyle( color: Colors.grey[500], fontSize: 12.sp, ), ), ], ), ), ...List.generate( _cruxList.length, (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), ), ), ), ), ], ), ); } Widget _inputBox() { return Container( height: 42.w, padding: EdgeInsets.symmetric(horizontal: 10.w), 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: [ 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; } }