AI助手优化-头像和对话功能
This commit is contained in:
@@ -4,7 +4,6 @@ import '../api/ai_api.dart';
|
||||
import '../core/network/api_exception.dart';
|
||||
import '../models/ai.dart';
|
||||
import 'ai_config_provider.dart';
|
||||
import 'ai_persona_provider.dart';
|
||||
import 'bill_refresh.dart';
|
||||
import 'note_provider.dart';
|
||||
|
||||
@@ -36,6 +35,15 @@ class AssistantState {
|
||||
/// 正在拉历史消息
|
||||
final bool loadingHistory;
|
||||
|
||||
/// 当前这次对话锁定的**人设 id**。null = 内置默认人设。
|
||||
///
|
||||
/// 这是「一次对话从头到尾是同一个人」的实现点:
|
||||
/// · 新建会话时定下来(用户在选人设弹层里选的,或默认用全局那个)
|
||||
/// · 打开历史会话时从会话记录里读出来
|
||||
/// · 之后每次 send 都用它,**不再看全局的 selectedAiPersonaProvider** ——
|
||||
/// 否则用户在设置页切一下人设,正在聊的这轮就换性格了
|
||||
final int? personaId;
|
||||
|
||||
const AssistantState({
|
||||
this.sessionId,
|
||||
this.bubbles = const [],
|
||||
@@ -43,6 +51,7 @@ class AssistantState {
|
||||
this.activeTool,
|
||||
this.error,
|
||||
this.loadingHistory = false,
|
||||
this.personaId,
|
||||
});
|
||||
|
||||
/// 真的一句都没有(不是加载中、也不是出错)
|
||||
@@ -55,6 +64,7 @@ class AssistantState {
|
||||
Object? activeTool = _sentinel,
|
||||
Object? error = _sentinel,
|
||||
bool? loadingHistory,
|
||||
Object? personaId = _sentinel,
|
||||
}) {
|
||||
return AssistantState(
|
||||
sessionId: sessionId == _sentinel ? this.sessionId : sessionId as int?,
|
||||
@@ -63,6 +73,7 @@ class AssistantState {
|
||||
activeTool: activeTool == _sentinel ? this.activeTool : activeTool as String?,
|
||||
error: error == _sentinel ? this.error : error as String?,
|
||||
loadingHistory: loadingHistory ?? this.loadingHistory,
|
||||
personaId: personaId == _sentinel ? this.personaId : personaId as int?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,16 +110,23 @@ class AssistantNotifier extends Notifier<AssistantState> {
|
||||
// 会话列表本来就是按最近活跃倒序,第一条就是「上一次的对话」
|
||||
final sessions = await ref.read(aiApiProvider).sessions();
|
||||
if (!ref.mounted || sessions.isEmpty) return;
|
||||
await openSession(sessions.first.id);
|
||||
// 带上这个人设 —— 恢复出来的对话得用它自己绑定的那个,不是全局偏好
|
||||
await openSession(sessions.first.id, personaId: sessions.first.personaId);
|
||||
} catch (_) {
|
||||
// 拉不到就算了,用户还能从「历史对话」里自己找
|
||||
}
|
||||
}
|
||||
|
||||
/// 新对话:清空界面。后端那条旧会话还在,能从历史里翻出来。
|
||||
void newChat() {
|
||||
/// 新对话。
|
||||
///
|
||||
/// [personaId] 是这次对话要用的人设,**传了就锁定**,之后这轮不会再变。
|
||||
/// 传 null 表示用内置默认人设。
|
||||
///
|
||||
/// 注意这里**不传全局的 selectedAiPersonaProvider** —— 那是「默认选哪套」的偏好,
|
||||
/// 由调用方(页面上的选人设弹层)决定要不要用它兜底。
|
||||
void newChat({int? personaId}) {
|
||||
_autoRestored = true;
|
||||
state = const AssistantState();
|
||||
state = AssistantState(personaId: personaId);
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
@@ -116,19 +134,28 @@ class AssistantNotifier extends Notifier<AssistantState> {
|
||||
}
|
||||
|
||||
/// 打开历史会话
|
||||
Future<void> openSession(int sessionId) async {
|
||||
state = AssistantState(sessionId: sessionId, loadingHistory: true);
|
||||
///
|
||||
/// 人设从**会话记录**里读,而不是用全局偏好 —— 三个月前那次对话是谁聊的,
|
||||
/// 现在打开还得是谁,不能因为你现在改了默认人设就变。
|
||||
Future<void> openSession(int sessionId, {int? personaId}) async {
|
||||
state = AssistantState(
|
||||
sessionId: sessionId,
|
||||
personaId: personaId,
|
||||
loadingHistory: true,
|
||||
);
|
||||
try {
|
||||
final items = await ref.read(aiApiProvider).messages(sessionId);
|
||||
if (!ref.mounted) return;
|
||||
state = AssistantState(
|
||||
sessionId: sessionId,
|
||||
personaId: personaId,
|
||||
bubbles: items.map(ChatBubble.fromItem).toList(),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!ref.mounted) return;
|
||||
state = AssistantState(
|
||||
sessionId: sessionId,
|
||||
personaId: personaId,
|
||||
error: e is ApiException ? e.message : '消息加载失败',
|
||||
);
|
||||
}
|
||||
@@ -159,10 +186,15 @@ class AssistantNotifier extends Notifier<AssistantState> {
|
||||
// 这次对话动过哪些数据
|
||||
final mutated = <_Mutated>{};
|
||||
try {
|
||||
// 带上用户选中的配置。null = 用服务端 .env 那套(虚拟的「默认」项)
|
||||
// 模型还是全局偏好(换模型是想马上生效的)
|
||||
final configId = ref.read(selectedAiConfigProvider);
|
||||
// 人设是独立的另一套。null = 用服务端内置的默认人设
|
||||
final personaId = ref.read(selectedAiPersonaProvider);
|
||||
|
||||
// 人设**不用全局偏好,用这次对话锁定的那个**。
|
||||
//
|
||||
// 这是「一个会话里始终是同一个人设」的关键:state.personaId 在新建会话
|
||||
// 或打开历史会话时就定了,中途用户在设置页怎么换都不影响正在聊的这轮。
|
||||
// 想换人设得开新会话 —— 这正是需求要的语义。
|
||||
final personaId = state.personaId;
|
||||
await for (final event in ref.read(aiApiProvider).chat(
|
||||
sessionId: state.sessionId,
|
||||
configId: configId,
|
||||
|
||||
@@ -41,11 +41,26 @@ class AuthNotifier extends Notifier<AuthState> {
|
||||
final user = UserInfo(
|
||||
username: response.username ?? '',
|
||||
nickname: response.nickname ?? response.username ?? '',
|
||||
avatar: response.avatar,
|
||||
);
|
||||
await LocalStorage.instance.setAuth(token: response.token, userInfo: user.toJson());
|
||||
state = AuthState(token: response.token, user: user);
|
||||
}
|
||||
|
||||
/// 改完资料后刷新本地缓存的用户信息。
|
||||
///
|
||||
/// 不发新 token —— 后端返回的 [AuthResponse.token] 是 null,
|
||||
/// 所以这里保留原来的 token,只换 user。
|
||||
Future<void> updateUserFromResponse(AuthResponse response) async {
|
||||
final user = UserInfo(
|
||||
username: response.username ?? state.user?.username ?? '',
|
||||
nickname: response.nickname ?? state.user?.nickname ?? '',
|
||||
avatar: response.avatar,
|
||||
);
|
||||
await LocalStorage.instance.setUserInfo(user.toJson());
|
||||
state = AuthState(token: state.token, user: user);
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
await LocalStorage.instance.clearAuth();
|
||||
state = const AuthState.initial();
|
||||
|
||||
Reference in New Issue
Block a user