AI 接入 V1.0
UI更新
This commit is contained in:
@@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../api/ai_api.dart';
|
||||
import '../core/network/api_exception.dart';
|
||||
import '../models/ai.dart';
|
||||
import 'bill_refresh.dart';
|
||||
import 'note_provider.dart';
|
||||
|
||||
/// 后端 AI 是否配好了(没配 key 时 App 显示提示,而不是让用户白问一句)
|
||||
final aiStatusProvider = FutureProvider.autoDispose<bool>((ref) {
|
||||
@@ -65,12 +67,45 @@ class AssistantState {
|
||||
static const _sentinel = Object();
|
||||
}
|
||||
|
||||
/// 会让服务端数据发生变化的工具。**名字要和后端 AiToolService 保持一致** ——
|
||||
/// 改名了这里就会静默失效(不报错,只是数据不刷新)。
|
||||
const _mutatingTools = {
|
||||
'create_bill': _Mutated.bill,
|
||||
'create_note': _Mutated.note,
|
||||
};
|
||||
|
||||
enum _Mutated { bill, note }
|
||||
|
||||
class AssistantNotifier extends Notifier<AssistantState> {
|
||||
/// 本次 App 运行期间是否已经自动恢复过。
|
||||
/// 用它拦住「用户主动开了新对话,切个 Tab 回来旧对话又被拉回来」。
|
||||
bool _autoRestored = false;
|
||||
|
||||
@override
|
||||
AssistantState build() => const AssistantState();
|
||||
|
||||
/// 新对话:清空界面,但**不清 sessionId 为 null 之前的会话**(后端那边还在,能从历史里翻出来)
|
||||
/// 打开助手页时调用:界面还是空的就把最近一次对话展开。
|
||||
///
|
||||
/// 为什么放在页面里而不是 provider 的 build():build() 里不能同步改 state,
|
||||
/// 而这个恢复动作是异步的、而且只该在这个页面被打开时发生。
|
||||
Future<void> restoreLastSessionIfNeeded() async {
|
||||
if (_autoRestored) return;
|
||||
_autoRestored = true;
|
||||
if (state.sessionId != null || state.bubbles.isNotEmpty) return;
|
||||
|
||||
try {
|
||||
// 会话列表本来就是按最近活跃倒序,第一条就是「上一次的对话」
|
||||
final sessions = await ref.read(aiApiProvider).sessions();
|
||||
if (!ref.mounted || sessions.isEmpty) return;
|
||||
await openSession(sessions.first.id);
|
||||
} catch (_) {
|
||||
// 拉不到就算了,用户还能从「历史对话」里自己找
|
||||
}
|
||||
}
|
||||
|
||||
/// 新对话:清空界面。后端那条旧会话还在,能从历史里翻出来。
|
||||
void newChat() {
|
||||
_autoRestored = true;
|
||||
state = const AssistantState();
|
||||
}
|
||||
|
||||
@@ -119,6 +154,8 @@ class AssistantNotifier extends Notifier<AssistantState> {
|
||||
// SSE 的收尾阶段出错(服务端异步派发、代理截断)很常见,
|
||||
// 弹个红条出来只会让人以为回答没拿到。
|
||||
var sawDone = false;
|
||||
// 这次对话动过哪些数据
|
||||
final mutated = <_Mutated>{};
|
||||
try {
|
||||
await for (final event in ref
|
||||
.read(aiApiProvider)
|
||||
@@ -129,6 +166,8 @@ class AssistantNotifier extends Notifier<AssistantState> {
|
||||
case AiEventType.delta:
|
||||
_appendDelta(event.content ?? '');
|
||||
case AiEventType.tool:
|
||||
final what = _mutatingTools[event.toolName];
|
||||
if (what != null) mutated.add(what);
|
||||
state = state.copyWith(activeTool: event.toolName);
|
||||
case AiEventType.done:
|
||||
// 新会话时后端在这一刻才把 ID 告诉我们,必须记下来,
|
||||
@@ -150,6 +189,28 @@ class AssistantNotifier extends Notifier<AssistantState> {
|
||||
}
|
||||
|
||||
_finish(failed: failed);
|
||||
_refreshMutatedData(mutated);
|
||||
}
|
||||
|
||||
/// 让被 AI 改过的数据在 App 里失效重取。
|
||||
///
|
||||
/// 为什么必须显式做:AI 是在**服务端直接写库**的,App 毫不知情。
|
||||
/// 而这些数据的 provider 被常驻页面(首页仪表盘的模块卡片、「我的」页)
|
||||
/// 一直监听着,永远不会被 autoDispose 回收 —— 不失效就是旧数。
|
||||
/// 表现会很迷惑:账单列表反而刷新了(它是 push 出来的页面,provider 被回收过),
|
||||
/// 于是看起来"列表对了、汇总和预算环没对"。
|
||||
void _refreshMutatedData(Set<_Mutated> mutated) {
|
||||
if (mutated.contains(_Mutated.bill)) {
|
||||
invalidateBillData(ref);
|
||||
}
|
||||
if (mutated.contains(_Mutated.note)) {
|
||||
// 四个都要失效:列表是 keepAlive 的 Notifier;概览被首页的笔记卡片常驻监听;
|
||||
// 标签和目录计数会随新建的笔记一起变
|
||||
ref.invalidate(noteListProvider);
|
||||
ref.invalidate(noteOverviewProvider);
|
||||
ref.invalidate(noteTagsProvider);
|
||||
ref.invalidate(noteFolderTreeProvider);
|
||||
}
|
||||
}
|
||||
|
||||
void _appendDelta(String delta) {
|
||||
|
||||
Reference in New Issue
Block a user