import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../api/ai_api.dart'; import '../core/network/api_exception.dart'; import '../models/ai.dart'; /// 后端 AI 是否配好了(没配 key 时 App 显示提示,而不是让用户白问一句) final aiStatusProvider = FutureProvider.autoDispose((ref) { return ref.watch(aiApiProvider).status(); }); /// 会话列表 final chatSessionsProvider = FutureProvider.autoDispose>((ref) { return ref.watch(aiApiProvider).sessions(); }); /// 助手页状态 class AssistantState { /// null = 新对话(还没落库,第一条消息发出去时后端才建) final int? sessionId; final List bubbles; /// 正在生成回答 final bool streaming; /// 正在调用哪个工具(null = 没在查东西) final String? activeTool; final String? error; /// 正在拉历史消息 final bool loadingHistory; const AssistantState({ this.sessionId, this.bubbles = const [], this.streaming = false, this.activeTool, this.error, this.loadingHistory = false, }); /// 真的一句都没有(不是加载中、也不是出错) bool get isEmpty => bubbles.isEmpty && !loadingHistory && error == null; AssistantState copyWith({ Object? sessionId = _sentinel, List? bubbles, bool? streaming, Object? activeTool = _sentinel, Object? error = _sentinel, bool? loadingHistory, }) { return AssistantState( sessionId: sessionId == _sentinel ? this.sessionId : sessionId as int?, bubbles: bubbles ?? this.bubbles, streaming: streaming ?? this.streaming, activeTool: activeTool == _sentinel ? this.activeTool : activeTool as String?, error: error == _sentinel ? this.error : error as String?, loadingHistory: loadingHistory ?? this.loadingHistory, ); } static const _sentinel = Object(); } class AssistantNotifier extends Notifier { @override AssistantState build() => const AssistantState(); /// 新对话:清空界面,但**不清 sessionId 为 null 之前的会话**(后端那边还在,能从历史里翻出来) void newChat() { state = const AssistantState(); } void clearError() { state = state.copyWith(error: null); } /// 打开历史会话 Future openSession(int sessionId) async { state = AssistantState(sessionId: sessionId, loadingHistory: true); try { final items = await ref.read(aiApiProvider).messages(sessionId); if (!ref.mounted) return; state = AssistantState( sessionId: sessionId, bubbles: items.map(ChatBubble.fromItem).toList(), ); } catch (e) { if (!ref.mounted) return; state = AssistantState( sessionId: sessionId, error: e is ApiException ? e.message : '消息加载失败', ); } } /// 发一条消息并接收流式回答 Future send(String text) async { final trimmed = text.trim(); if (trimmed.isEmpty || state.streaming) return; state = state.copyWith( // 先乐观地把用户气泡和「空的、正在生成」的回答气泡放上去 bubbles: [ ...state.bubbles, ChatBubble.user(trimmed), const ChatBubble.assistant('', streaming: true), ], streaming: true, error: null, activeTool: null, ); var failed = false; // 收到 done 说明回答已经完整了,之后连接怎么断都不该算失败 —— // SSE 的收尾阶段出错(服务端异步派发、代理截断)很常见, // 弹个红条出来只会让人以为回答没拿到。 var sawDone = false; try { await for (final event in ref .read(aiApiProvider) .chat(sessionId: state.sessionId, content: trimmed)) { if (!ref.mounted) return; switch (event.type) { case AiEventType.delta: _appendDelta(event.content ?? ''); case AiEventType.tool: state = state.copyWith(activeTool: event.toolName); case AiEventType.done: // 新会话时后端在这一刻才把 ID 告诉我们,必须记下来, // 否则下一条消息又会开一个新会话 sawDone = true; state = state.copyWith(sessionId: event.sessionId); case AiEventType.error: if (!sawDone) { failed = true; state = state.copyWith(error: event.message); } } } } catch (e) { if (!sawDone) { failed = true; state = state.copyWith(error: e is ApiException ? e.message : 'AI 连接中断'); } } _finish(failed: failed); } void _appendDelta(String delta) { if (delta.isEmpty) return; final bubbles = [...state.bubbles]; if (bubbles.isEmpty) return; final last = bubbles.last; bubbles[bubbles.length - 1] = last.copyWith(content: last.content + delta); // 有文字出来了,说明工具已经查完 state = state.copyWith(bubbles: bubbles, activeTool: null); } void _finish({required bool failed}) { var bubbles = [...state.bubbles]; if (failed) { // 一条字都没生成出来时,把那个空气泡去掉,只留错误提示 if (bubbles.isNotEmpty && !bubbles.last.isUser && bubbles.last.content.isEmpty) { bubbles.removeLast(); } } else if (bubbles.isNotEmpty && bubbles.last.streaming) { bubbles[bubbles.length - 1] = bubbles.last.copyWith(streaming: false); } state = state.copyWith(bubbles: bubbles, streaming: false, activeTool: null); if (!failed) { // 新会话要出现在列表里、老会话的标题/时间要更新 ref.invalidate(chatSessionsProvider); } } } /// 不加 autoDispose:切到别的 Tab 再回来,对话还在 final assistantProvider = NotifierProvider(AssistantNotifier.new);