import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'auth_provider.dart'; import 'data_providers.dart'; import '../utils/sse_handler.dart'; class ChatMessage { final String id; final String role; String content; final DateTime createdAt; ChatMessage( {required this.id, required this.role, required this.content, required this.createdAt}); bool get isUser => role == 'user'; } enum ActiveAgent { default_, consultation, health, diet, medication, report, exercise } class ChatState { final ActiveAgent activeAgent; final List messages; final String? conversationId; final bool isStreaming; final String? noticeText; const ChatState({ this.activeAgent = ActiveAgent.default_, this.messages = const [], this.conversationId, this.isStreaming = false, this.noticeText, }); ChatState copyWith({ActiveAgent? activeAgent, List? messages, String? conversationId, bool? isStreaming, String? noticeText, bool clearNotice = false}) => ChatState( activeAgent: activeAgent ?? this.activeAgent, messages: messages ?? this.messages, conversationId: conversationId ?? this.conversationId, isStreaming: isStreaming ?? this.isStreaming, noticeText: clearNotice ? null : (noticeText ?? this.noticeText), ); } class SelectedAgentNotifier extends Notifier { @override ActiveAgent? build() => null; void select(ActiveAgent? a) => state = a; } final selectedAgentProvider = NotifierProvider(SelectedAgentNotifier.new); final chatProvider = NotifierProvider(ChatNotifier.new); class ChatNotifier extends Notifier { StreamSubscription>? _subscription; @override ChatState build() => const ChatState(); void setAgent(ActiveAgent a) { _subscription?.cancel(); state = state.activeAgent == a ? const ChatState() : ChatState(activeAgent: a); } Future sendMessage(String text) async { if (text.trim().isEmpty || state.isStreaming) return; final userMsg = ChatMessage( id: '${DateTime.now().millisecondsSinceEpoch}', role: 'user', content: text, createdAt: DateTime.now(), ); state = state.copyWith( messages: [...state.messages, userMsg], isStreaming: true); final aiMsg = ChatMessage( id: '${DateTime.now().millisecondsSinceEpoch}_ai', role: 'assistant', content: '', createdAt: DateTime.now(), ); try { final token = await ref.read(apiClientProvider).accessToken; if (token == null) { _addError(aiMsg, '未登录,请重新登录'); return; } final agentPath = state.activeAgent.name.replaceFirst('default_', 'default'); final stream = SseHandler.connect( agentType: agentPath, message: text, conversationId: state.conversationId, token: token, ); await for (final event in stream) { _processEvent(event, aiMsg); } } catch (e) { _addError(aiMsg, '网络异常,请稍后重试'); } } void _addError(ChatMessage aiMsg, String errorText) { state = state.copyWith( messages: [ ...state.messages, ChatMessage( id: 'err_${DateTime.now().millisecondsSinceEpoch}', role: 'assistant', content: errorText, createdAt: DateTime.now(), ), ], isStreaming: false, clearNotice: true, ); } void _processEvent(Map j, ChatMessage aiMsg) { final a = j['action'] as String?; switch (a) { case 'conversation_id': state = state.copyWith(conversationId: j['data']?.toString()); case 'answer': aiMsg.content += (j['data'] as String?) ?? ''; _update(aiMsg); case 'notice': state = state.copyWith(noticeText: j['message'] as String?); case 'tool_result': final tool = j['tool'] as String? ?? ''; if (tool == 'record_health_data') { refreshHealthData(ref); } case 'status': _done(aiMsg); case 'error': _done(aiMsg); } } void _update(ChatMessage m) { final u = state.messages.toList(); final i = u.indexWhere((x) => x.id == m.id); if (i >= 0) { u[i] = m; } else if (m.content.isNotEmpty) { u.add(m); } state = state.copyWith(messages: u); } void _done(ChatMessage m) { final u = state.messages.toList(); if (!u.any((x) => x.id == m.id) && m.content.isNotEmpty) u.add(m); state = state.copyWith(messages: u, isStreaming: false, clearNotice: true); } }