import 'dart:async'; import 'dart:io'; import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:record/record.dart'; import '../models/chat_models.dart'; import '../services/http_api_service.dart'; import 'debug_provider.dart'; class ChatProvider extends ChangeNotifier { ChatProvider({ required HttpApiService httpApiService, required DebugProvider debugProvider, }) : _http = httpApiService, _debug = debugProvider; final HttpApiService _http; final DebugProvider _debug; final AudioRecorder _recorder = AudioRecorder(); final AudioPlayer _player = AudioPlayer(); final List _messages = []; String _sessionId = 'app_${DateTime.now().millisecondsSinceEpoch}'; bool _busy = false; bool _recording = false; String? _error; String? _status; List get messages => List.unmodifiable(_messages); bool get isBusy => _busy; bool get isRecording => _recording; String? get error => _error; String? get status => _status; String get sessionId => _sessionId; void clearHistory() { _messages.clear(); _sessionId = 'app_${DateTime.now().millisecondsSinceEpoch}'; _error = null; _status = '已清空对话'; notifyListeners(); } Future sendText(String text) async { final content = text.trim(); if (content.isEmpty || _busy) return; _messages.add(ChatBubble(role: ChatBubbleRole.user, text: content)); final pending = ChatBubble( role: ChatBubbleRole.assistant, text: '思考中...', isPending: true, ); _messages.add(pending); _busy = true; _error = null; _status = '角色思考中...'; notifyListeners(); try { final response = await _http.chatText( text: content, sessionId: _sessionId, ); await _applyResponse(pending, response); } catch (error) { pending ..isPending = false ..text = '请求失败: $error'; _error = error.toString(); _debug.addHttpLog('chatText failed', details: _error); } finally { _busy = false; if (_status == '角色思考中...') { _status = null; } notifyListeners(); } } Future startRecording() async { if (_busy || _recording) return false; try { final hasPermission = await _recorder.hasPermission(); if (!hasPermission) { _error = '需要麦克风权限才能语音对话'; _status = _error; notifyListeners(); return false; } final dir = await getTemporaryDirectory(); final path = '${dir.path}/showen_rec_${DateTime.now().millisecondsSinceEpoch}.wav'; await _recorder.start( const RecordConfig( encoder: AudioEncoder.wav, sampleRate: 16000, numChannels: 1, ), path: path, ); _recording = true; _error = null; _status = '正在录音... 松开发送'; _debug.addBleLog('Voice recording started'); notifyListeners(); return true; } catch (error) { _error = '无法开始录音: $error'; _status = _error; _debug.addHttpLog('startRecording failed', details: _error); notifyListeners(); return false; } } Future cancelRecording() async { if (!_recording) return; try { final path = await _recorder.stop(); if (path != null) { final file = File(path); if (await file.exists()) { await file.delete(); } } } catch (_) {} _recording = false; _status = '已取消录音'; notifyListeners(); } Future stopRecordingAndSend() async { if (!_recording || _busy) return; _recording = false; _status = '正在识别...'; notifyListeners(); String? path; try { path = await _recorder.stop(); } catch (error) { _error = '停止录音失败: $error'; _status = _error; notifyListeners(); return; } if (path == null) { _error = '没有录到音频'; _status = _error; notifyListeners(); return; } final file = File(path); if (!await file.exists()) { _error = '录音文件不存在'; _status = _error; notifyListeners(); return; } final bytes = await file.readAsBytes(); try { await file.delete(); } catch (_) {} if (bytes.length < 1000) { _error = '录音太短,请按住再说一会儿'; _status = _error; notifyListeners(); return; } _messages.add( ChatBubble(role: ChatBubbleRole.user, text: '🎤 语音消息'), ); final pending = ChatBubble( role: ChatBubbleRole.assistant, text: '思考中...', isPending: true, ); _messages.add(pending); _busy = true; _status = '角色思考中...'; notifyListeners(); try { final response = await _http.chatAudio( bytes: bytes, format: 'wav', sessionId: _sessionId, ); if (response.transcription != null && response.transcription!.trim().isNotEmpty) { // 回填用户气泡为转写文本 for (var i = _messages.length - 1; i >= 0; i--) { if (_messages[i].role == ChatBubbleRole.user) { _messages[i].text = response.transcription!; break; } } } await _applyResponse(pending, response); } catch (error) { pending ..isPending = false ..text = '语音对话失败: $error'; _error = error.toString(); _debug.addHttpLog('chatAudio failed', details: _error); } finally { _busy = false; if (_status == '角色思考中...') { _status = null; } notifyListeners(); } } Future _applyResponse( ChatBubble pending, ChatTurnResponse response, ) async { pending.isPending = false; if (!response.isOk) { pending.text = response.error ?? '对话失败'; _error = pending.text; _status = pending.text; return; } if (response.sessionId.isNotEmpty) { _sessionId = response.sessionId; } pending.text = response.replyText.isEmpty ? '(空回复)' : response.replyText; pending.audioPath = response.replyAudioPath; _status = '已回复'; _debug.addHttpLog( 'chat ok', details: 'chars=${pending.text.length} audio=${pending.audioPath != null}', ); if (pending.audioPath != null && pending.audioPath!.isNotEmpty) { unawaited(playReply(pending.audioPath!)); } } Future playReply(String absolutePath) async { try { final url = _http.chatAudioFileUrl(absolutePath); await _player.stop(); await _player.play(UrlSource(url)); _status = '正在播放回复...'; notifyListeners(); } catch (error) { _error = '播放失败: $error'; _debug.addHttpLog('playReply failed', details: _error); notifyListeners(); } } @override void dispose() { unawaited(_recorder.dispose()); unawaited(_player.dispose()); super.dispose(); } }