import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'auth_provider.dart'; class ConsultationMsg { final String id; final String senderType; // 'User' | 'Ai' | 'Doctor' final String? senderName; final String content; final DateTime createdAt; final List? quickOptions; ConsultationMsg({ required this.id, required this.senderType, this.senderName, required this.content, required this.createdAt, this.quickOptions, }); } class ConsultationChatState { final String? consultationId; final String doctorId; final String doctorName; final String doctorTitle; final String doctorDepartment; final String status; // AiTalking | WaitingDoctor | DoctorReplied | Closed final List messages; final bool isLoading; final bool isSending; final int quotaRemaining; final int quotaTotal; const ConsultationChatState({ this.consultationId, this.doctorId = '', this.doctorName = '', this.doctorTitle = '', this.doctorDepartment = '', this.status = 'AiTalking', this.messages = const [], this.isLoading = true, this.isSending = false, this.quotaRemaining = 3, this.quotaTotal = 3, }); ConsultationChatState copyWith({ String? consultationId, String? doctorId, String? doctorName, String? doctorTitle, String? doctorDepartment, String? status, List? messages, bool? isLoading, bool? isSending, int? quotaRemaining, int? quotaTotal, }) => ConsultationChatState( consultationId: consultationId ?? this.consultationId, doctorId: doctorId ?? this.doctorId, doctorName: doctorName ?? this.doctorName, doctorTitle: doctorTitle ?? this.doctorTitle, doctorDepartment: doctorDepartment ?? this.doctorDepartment, status: status ?? this.status, messages: messages ?? this.messages, isLoading: isLoading ?? this.isLoading, isSending: isSending ?? this.isSending, quotaRemaining: quotaRemaining ?? this.quotaRemaining, quotaTotal: quotaTotal ?? this.quotaTotal, ); } final consultationChatProvider = NotifierProvider( ConsultationChatNotifier.new); class ConsultationChatNotifier extends Notifier { Timer? _pollTimer; @override ConsultationChatState build() => const ConsultationChatState(); Future init(String doctorId) async { state = state.copyWith(doctorId: doctorId, isLoading: true); final api = ref.read(apiClientProvider); try { // 加载医生信息 await _loadDoctorInfo(doctorId); // 加载配额 final quotaRes = await api.get('/api/user/consultation-quota'); final quota = quotaRes.data['data']; state = state.copyWith( quotaRemaining: quota?['remaining'] ?? 3, quotaTotal: quota?['total'] ?? 3, ); // 创建问诊会话 final createRes = await api.post('/api/consultations', data: {'doctorId': doctorId}); final consultationId = createRes.data['data']?['id']?.toString() ?? ''; // 加载历史消息 await _loadMessages(consultationId); // 新会话:插入AI开场问候 if (state.messages.isEmpty) { final greeting = ConsultationMsg( id: 'greeting_${DateTime.now().millisecondsSinceEpoch}', senderType: 'Ai', senderName: 'AI分身 · ${state.doctorName}', content: '您好,我是${state.doctorName}的AI分身。请问您最近有什么身体不适?可以描述一下您的症状,我会先帮您做初步分析。\n\n如果情况需要,我会帮您转接${state.doctorName}医生。', createdAt: DateTime.now(), ); state = state.copyWith( consultationId: consultationId, messages: [greeting], isLoading: false, ); } else { state = state.copyWith(consultationId: consultationId, isLoading: false); } _startPolling(); } catch (_) { state = state.copyWith(isLoading: false); } } Future _loadDoctorInfo(String doctorId) async { try { final api = ref.read(apiClientProvider); final res = await api.get('/api/doctors'); final list = (res.data['data'] as List?) ?? []; final doc = list.cast>().firstWhere( (d) => d['id']?.toString() == doctorId, orElse: () => {}); state = state.copyWith( doctorName: doc['name']?.toString() ?? '', doctorTitle: doc['title']?.toString() ?? '', doctorDepartment: doc['department']?.toString() ?? '', ); } catch (_) {} } Future _loadMessages(String consultationId) async { try { final api = ref.read(apiClientProvider); final res = await api.get('/api/consultations/$consultationId/messages'); final list = (res.data['data'] as List?) ?? []; final msgs = list.map((m) { final map = m as Map; return ConsultationMsg( id: map['id']?.toString() ?? '', senderType: map['senderType']?.toString() ?? 'User', senderName: map['senderName']?.toString(), content: map['content']?.toString() ?? '', createdAt: DateTime.tryParse(map['createdAt']?.toString() ?? '') ?? DateTime.now(), ); }).toList(); if (msgs.isNotEmpty) { state = state.copyWith(messages: msgs); } } catch (_) {} } Future sendMessage(String text) async { if (text.trim().isEmpty || state.isSending || state.consultationId == null) return; final userMsg = ConsultationMsg( id: '${DateTime.now().millisecondsSinceEpoch}', senderType: 'User', content: text, createdAt: DateTime.now(), ); state = state.copyWith( messages: [...state.messages, userMsg], isSending: true, ); try { final api = ref.read(apiClientProvider); await api.post('/api/consultations/${state.consultationId}/messages', data: {'content': text}); state = state.copyWith(isSending: false); _pollMessages(); } catch (_) { state = state.copyWith(isSending: false); } } void _startPolling() { _pollTimer?.cancel(); _pollTimer = Timer.periodic(const Duration(seconds: 15), (_) => _pollMessages()); } Future _pollMessages() async { if (state.consultationId == null) return; try { final api = ref.read(apiClientProvider); final lastId = state.messages.isNotEmpty ? state.messages.last.id : null; final params = {}; if (lastId != null && lastId.startsWith('greeting_') == false) { params['after'] = lastId; } final res = await api.get( '/api/consultations/${state.consultationId}/messages', queryParameters: params.isNotEmpty ? params : null, ); final list = (res.data['data'] as List?) ?? []; if (list.isEmpty) return; final existingIds = state.messages.map((m) => m.id).toSet(); final newMsgs = list .map((m) { final map = m as Map; return ConsultationMsg( id: map['id']?.toString() ?? '', senderType: map['senderType']?.toString() ?? 'User', senderName: map['senderName']?.toString(), content: map['content']?.toString() ?? '', createdAt: DateTime.tryParse(map['createdAt']?.toString() ?? '') ?? DateTime.now(), ); }) .where((m) => !existingIds.contains(m.id)) .toList(); if (newMsgs.isNotEmpty) { state = state.copyWith(messages: [...state.messages, ...newMsgs]); } } catch (_) {} } void stopPolling() { _pollTimer?.cancel(); _pollTimer = null; } }