马良AI写作初始化仓库

This commit is contained in:
邓滨杰
2025-09-10 00:07:52 +08:00
parent 3c06bb1a03
commit 39c0f8840f
1309 changed files with 318528 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import '../../services/api_service/repositories/impl/admin_repository_impl.dart';
import '../../models/admin/admin_models.dart';
part 'admin_event.dart';
part 'admin_state.dart';
class AdminBloc extends Bloc<AdminEvent, AdminState> {
final AdminRepositoryImpl adminRepository;
AdminBloc(this.adminRepository) : super(AdminInitial()) {
on<LoadDashboardStats>(_onLoadDashboardStats);
on<LoadUsers>(_onLoadUsers);
on<LoadRoles>(_onLoadRoles);
on<LoadModelConfigs>(_onLoadModelConfigs);
on<LoadSystemConfigs>(_onLoadSystemConfigs);
on<UpdateUserStatus>(_onUpdateUserStatus);
on<CreateRole>(_onCreateRole);
on<UpdateRole>(_onUpdateRole);
on<UpdateModelConfig>(_onUpdateModelConfig);
on<UpdateSystemConfig>(_onUpdateSystemConfig);
on<AddCreditsToUser>(_onAddCreditsToUser);
on<DeductCreditsFromUser>(_onDeductCreditsFromUser);
on<UpdateUserInfo>(_onUpdateUserInfo);
on<AssignRoleToUser>(_onAssignRoleToUser);
}
Future<void> _onLoadDashboardStats(
LoadDashboardStats event,
Emitter<AdminState> emit,
) async {
emit(AdminLoading());
try {
final stats = await adminRepository.getDashboardStats();
emit(DashboardStatsLoaded(stats));
} catch (e) {
emit(AdminError(e.toString()));
}
}
Future<void> _onLoadUsers(
LoadUsers event,
Emitter<AdminState> emit,
) async {
emit(AdminLoading());
try {
final users = await adminRepository.getUsers(
page: event.page,
size: event.size,
search: event.search,
);
emit(UsersLoaded(users));
} catch (e) {
emit(AdminError(e.toString()));
}
}
Future<void> _onLoadRoles(
LoadRoles event,
Emitter<AdminState> emit,
) async {
emit(AdminLoading());
try {
final roles = await adminRepository.getRoles();
emit(RolesLoaded(roles));
} catch (e) {
emit(AdminError(e.toString()));
}
}
Future<void> _onLoadModelConfigs(
LoadModelConfigs event,
Emitter<AdminState> emit,
) async {
emit(AdminLoading());
try {
final configs = await adminRepository.getModelConfigs();
emit(ModelConfigsLoaded(configs));
} catch (e) {
emit(AdminError(e.toString()));
}
}
Future<void> _onLoadSystemConfigs(
LoadSystemConfigs event,
Emitter<AdminState> emit,
) async {
emit(AdminLoading());
try {
final configs = await adminRepository.getSystemConfigs();
emit(SystemConfigsLoaded(configs));
} catch (e) {
emit(AdminError(e.toString()));
}
}
Future<void> _onUpdateUserStatus(
UpdateUserStatus event,
Emitter<AdminState> emit,
) async {
try {
await adminRepository.updateUserStatus(event.userId, event.status);
// 重新加载用户列表
add(LoadUsers());
} catch (e) {
emit(AdminError(e.toString()));
}
}
Future<void> _onCreateRole(
CreateRole event,
Emitter<AdminState> emit,
) async {
try {
await adminRepository.createRole(event.role);
// 重新加载角色列表
add(LoadRoles());
} catch (e) {
emit(AdminError(e.toString()));
}
}
Future<void> _onUpdateRole(
UpdateRole event,
Emitter<AdminState> emit,
) async {
try {
await adminRepository.updateRole(event.roleId, event.role);
// 重新加载角色列表
add(LoadRoles());
} catch (e) {
emit(AdminError(e.toString()));
}
}
Future<void> _onUpdateModelConfig(
UpdateModelConfig event,
Emitter<AdminState> emit,
) async {
try {
await adminRepository.updateModelConfig(event.configId, event.config);
// 重新加载模型配置列表
add(LoadModelConfigs());
} catch (e) {
emit(AdminError(e.toString()));
}
}
Future<void> _onUpdateSystemConfig(
UpdateSystemConfig event,
Emitter<AdminState> emit,
) async {
try {
await adminRepository.updateSystemConfig(event.configKey, event.value);
// 重新加载系统配置列表
add(LoadSystemConfigs());
} catch (e) {
emit(AdminError(e.toString()));
}
}
Future<void> _onAddCreditsToUser(
AddCreditsToUser event,
Emitter<AdminState> emit,
) async {
try {
await adminRepository.addCreditsToUser(event.userId, event.amount, event.reason);
// 重新加载用户列表
add(LoadUsers());
} catch (e) {
emit(AdminError(e.toString()));
}
}
Future<void> _onDeductCreditsFromUser(
DeductCreditsFromUser event,
Emitter<AdminState> emit,
) async {
try {
await adminRepository.deductCreditsFromUser(event.userId, event.amount, event.reason);
// 重新加载用户列表
add(LoadUsers());
} catch (e) {
emit(AdminError(e.toString()));
}
}
Future<void> _onUpdateUserInfo(
UpdateUserInfo event,
Emitter<AdminState> emit,
) async {
try {
await adminRepository.updateUserInfo(
event.userId,
email: event.email,
displayName: event.displayName,
accountStatus: event.accountStatus,
);
// 重新加载用户列表
add(LoadUsers());
} catch (e) {
emit(AdminError(e.toString()));
}
}
Future<void> _onAssignRoleToUser(
AssignRoleToUser event,
Emitter<AdminState> emit,
) async {
try {
await adminRepository.assignRoleToUser(event.userId, event.roleId);
// 重新加载用户列表
add(LoadUsers());
} catch (e) {
emit(AdminError(e.toString()));
}
}
}

View File

@@ -0,0 +1,153 @@
part of 'admin_bloc.dart';
abstract class AdminEvent extends Equatable {
const AdminEvent();
@override
List<Object?> get props => [];
}
class LoadDashboardStats extends AdminEvent {}
class LoadUsers extends AdminEvent {
final int page;
final int size;
final String? search;
const LoadUsers({
this.page = 0,
this.size = 20,
this.search,
});
@override
List<Object?> get props => [page, size, search];
}
class LoadRoles extends AdminEvent {}
class LoadModelConfigs extends AdminEvent {}
class LoadSystemConfigs extends AdminEvent {}
class UpdateUserStatus extends AdminEvent {
final String userId;
final String status;
const UpdateUserStatus({
required this.userId,
required this.status,
});
@override
List<Object> get props => [userId, status];
}
class CreateRole extends AdminEvent {
final AdminRole role;
const CreateRole(this.role);
@override
List<Object> get props => [role];
}
class UpdateRole extends AdminEvent {
final String roleId;
final AdminRole role;
const UpdateRole({
required this.roleId,
required this.role,
});
@override
List<Object> get props => [roleId, role];
}
class UpdateModelConfig extends AdminEvent {
final String configId;
final AdminModelConfig config;
const UpdateModelConfig({
required this.configId,
required this.config,
});
@override
List<Object> get props => [configId, config];
}
class UpdateSystemConfig extends AdminEvent {
final String configKey;
final String value;
const UpdateSystemConfig({
required this.configKey,
required this.value,
});
@override
List<Object> get props => [configKey, value];
}
// 添加积分管理相关事件
class AddCreditsToUser extends AdminEvent {
final String userId;
final int amount;
final String reason;
const AddCreditsToUser({
required this.userId,
required this.amount,
required this.reason,
});
@override
List<Object> get props => [userId, amount, reason];
}
class DeductCreditsFromUser extends AdminEvent {
final String userId;
final int amount;
final String reason;
const DeductCreditsFromUser({
required this.userId,
required this.amount,
required this.reason,
});
@override
List<Object> get props => [userId, amount, reason];
}
class UpdateUserInfo extends AdminEvent {
final String userId;
final String? email;
final String? displayName;
final String? accountStatus;
const UpdateUserInfo({
required this.userId,
this.email,
this.displayName,
this.accountStatus,
});
@override
List<Object?> get props => [userId, email, displayName, accountStatus];
}
class AssignRoleToUser extends AdminEvent {
final String userId;
final String roleId;
const AssignRoleToUser({
required this.userId,
required this.roleId,
});
@override
List<Object> get props => [userId, roleId];
}

View File

@@ -0,0 +1,66 @@
part of 'admin_bloc.dart';
abstract class AdminState extends Equatable {
const AdminState();
@override
List<Object?> get props => [];
}
class AdminInitial extends AdminState {}
class AdminLoading extends AdminState {}
class AdminError extends AdminState {
final String message;
const AdminError(this.message);
@override
List<Object> get props => [message];
}
class DashboardStatsLoaded extends AdminState {
final AdminDashboardStats stats;
const DashboardStatsLoaded(this.stats);
@override
List<Object> get props => [stats];
}
class UsersLoaded extends AdminState {
final List<AdminUser> users;
const UsersLoaded(this.users);
@override
List<Object> get props => [users];
}
class RolesLoaded extends AdminState {
final List<AdminRole> roles;
const RolesLoaded(this.roles);
@override
List<Object> get props => [roles];
}
class ModelConfigsLoaded extends AdminState {
final List<AdminModelConfig> configs;
const ModelConfigsLoaded(this.configs);
@override
List<Object> get props => [configs];
}
class SystemConfigsLoaded extends AdminState {
final List<AdminSystemConfig> configs;
const SystemConfigsLoaded(this.configs);
@override
List<Object> get props => [configs];
}

View File

@@ -0,0 +1,746 @@
import 'package:ainoval/models/user_ai_model_config_model.dart';
import 'package:ainoval/models/ai_model_group.dart';
import 'package:ainoval/models/model_info.dart'; // Import ModelInfo
import 'package:ainoval/services/api_service/repositories/user_ai_model_config_repository.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart'; // For firstWhereOrNull
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; // For ValueGetter
part 'ai_config_event.dart';
part 'ai_config_state.dart';
class AiConfigBloc extends Bloc<AiConfigEvent, AiConfigState> {
AiConfigBloc({required UserAIModelConfigRepository repository})
: _repository = repository,
super(const AiConfigState()) {
on<LoadAiConfigs>(_onLoadAiConfigs);
on<ResetAiConfigs>(_onResetAiConfigs);
on<LoadAvailableProviders>(_onLoadAvailableProviders);
on<LoadModelsForProvider>(_onLoadModelsForProvider);
on<AddAiConfig>(_onAddAiConfig);
on<UpdateAiConfig>(_onUpdateAiConfig);
on<DeleteAiConfig>(_onDeleteAiConfig);
on<ValidateAiConfig>(_onValidateAiConfig);
on<SetDefaultAiConfig>(_onSetDefaultAiConfig);
on<ClearProviderModels>(_onClearProviderModels);
on<GetProviderDefaultConfig>(_onGetProviderDefaultConfig);
on<LoadApiKeyForConfig>(_onLoadApiKeyForConfig);
on<LoadProviderCapability>(_onLoadProviderCapability);
on<TestApiKey>(_onTestApiKey);
on<ClearApiKeyTestError>(_onClearApiKeyTestError);
on<ClearModelsCache>(_onClearModelsCache);
on<AddCustomModelAndValidate>(_onAddCustomModelAndValidate);
}
final UserAIModelConfigRepository _repository;
// 添加缓存机制
DateTime? _lastConfigsLoadTime;
static const Duration _cacheValidDuration = Duration(minutes: 5);
// 记录上一次加载配置对应的用户,用于跨用户时强制刷新
String? _lastLoadedUserId;
// 添加模型列表缓存机制
Map<String, DateTime> _modelsCacheTime = {};
static const Duration _modelsCacheValidDuration = Duration(minutes: 10);
// 添加提供商列表缓存机制
DateTime? _lastProvidersLoadTime;
static const Duration _providersCacheDuration = Duration(hours: 1);
bool get _shouldRefreshConfigs {
if (_lastConfigsLoadTime == null) return true;
return DateTime.now().difference(_lastConfigsLoadTime!) > _cacheValidDuration;
}
bool get _shouldRefreshProviders {
if (_lastProvidersLoadTime == null) return true;
return DateTime.now().difference(_lastProvidersLoadTime!) > _providersCacheDuration;
}
// 检查特定提供商的模型列表缓存是否有效
bool _shouldRefreshModels(String provider) {
// 如果状态中没有该提供商的模型数据,需要加载
if (!state.modelGroups.containsKey(provider) ||
state.modelGroups[provider]?.allModelsInfo.isEmpty == true) {
return true;
}
// 检查缓存时间
final lastLoadTime = _modelsCacheTime[provider];
if (lastLoadTime == null) {
// 模型数据已存在但没有记录时间戳,认为仍然有效,补记录当前时间
_modelsCacheTime[provider] = DateTime.now();
return false;
}
return DateTime.now().difference(lastLoadTime) > _modelsCacheValidDuration;
}
/// Helper方法根据配置列表重新构建providerDefaultConfigs
Map<String, UserAIModelConfigModel> _buildProviderDefaultConfigs(
List<UserAIModelConfigModel> configs) {
final Map<String, UserAIModelConfigModel> providerDefaultConfigs = {};
// 按提供商分组
final configsByProvider = <String, List<UserAIModelConfigModel>>{};
for (final config in configs) {
if (!configsByProvider.containsKey(config.provider)) {
configsByProvider[config.provider] = [];
}
configsByProvider[config.provider]!.add(config);
}
// 为每个提供商选择一个默认配置
configsByProvider.forEach((provider, providerConfigs) {
// 优先选择默认配置,其次是已验证的配置,最后选择第一个配置
final defaultConfig = providerConfigs.firstWhere(
(c) => c.isDefault,
orElse: () => providerConfigs.firstWhere(
(c) => c.isValidated,
orElse: () => providerConfigs.first,
),
);
providerDefaultConfigs[provider] = defaultConfig;
});
return providerDefaultConfigs;
}
Future<void> _onLoadAiConfigs(
LoadAiConfigs event, Emitter<AiConfigState> emit) async {
// 如果用户已切换,强制刷新缓存与状态
if (_lastLoadedUserId != null && _lastLoadedUserId != event.userId) {
_lastConfigsLoadTime = null;
}
// 检查缓存是否有效
if (!_shouldRefreshConfigs && state.configs.isNotEmpty) {
AppLogger.d('AiConfigBloc', '使用缓存的配置数据,跳过重新加载');
return;
}
emit(state.copyWith(status: AiConfigStatus.loading));
try {
final configs =
await _repository.listConfigurations(userId: event.userId);
_lastConfigsLoadTime = DateTime.now(); // 更新缓存时间
// 按提供商分组用户配置
final providerDefaultConfigs = _buildProviderDefaultConfigs(configs);
emit(state.copyWith(
status: AiConfigStatus.loaded,
configs: configs,
providerDefaultConfigs: providerDefaultConfigs,
errorMessage: () => null, // Clear previous error
));
// 记录当前加载用户
_lastLoadedUserId = event.userId;
AppLogger.i('AiConfigBloc', '配置加载成功,共${configs.length}个配置,已缓存');
} catch (e, stackTrace) {
AppLogger.e('AiConfigBloc', '加载配置失败', e, stackTrace);
emit(state.copyWith(
status: AiConfigStatus.error, errorMessage: () => e.toString()));
}
}
// 重置事件:清空状态与所有相关缓存(用于登出/切换账号)
void _onResetAiConfigs(ResetAiConfigs event, Emitter<AiConfigState> emit) {
_lastConfigsLoadTime = null;
_lastProvidersLoadTime = null;
_modelsCacheTime.clear();
_lastLoadedUserId = null;
emit(const AiConfigState());
AppLogger.i('AiConfigBloc', '已重置AI配置状态与缓存');
}
Future<void> _onLoadAvailableProviders(
LoadAvailableProviders event, Emitter<AiConfigState> emit) async {
// 如果已有缓存且未过期,直接返回
if (!_shouldRefreshProviders && state.availableProviders.isNotEmpty) {
AppLogger.d('AiConfigBloc', '使用缓存的提供商列表,跳过重新加载');
return;
}
try {
final providers = await _repository.listAvailableProviders();
_lastProvidersLoadTime = DateTime.now();
emit(state.copyWith(
availableProviders: providers,
errorMessage: () => null,
));
} catch (e, stackTrace) {
AppLogger.e('AiConfigBloc', '加载提供商失败', e, stackTrace);
emit(state.copyWith(errorMessage: () => '加载提供商列表失败: \\${e.toString()}'));
}
}
Future<void> _onLoadModelsForProvider(
LoadModelsForProvider event, Emitter<AiConfigState> emit) async {
// 检查缓存是否有效
if (!_shouldRefreshModels(event.provider)) {
AppLogger.d('AiConfigBloc', '使用缓存的模型数据,跳过重新加载: provider=${event.provider}');
// 更新selectedProviderForModels以确保UI正确显示
final cachedModelGroup = state.modelGroups[event.provider];
if (cachedModelGroup != null) {
emit(state.copyWith(
selectedProviderForModels: event.provider,
modelsForProviderInfo: cachedModelGroup.allModelsInfo,
));
// 仍然触发GetProviderDefaultConfig以确保默认配置正确加载
add(GetProviderDefaultConfig(provider: event.provider));
}
return;
}
emit(state.copyWith(
modelsForProviderInfo: [],
selectedProviderForModels: event.provider,
apiKeyTestSuccessProviderClearable: () => null,
apiKeyTestErrorClearable: () => null,
));
try {
final models = await _repository.listModelsForProvider(event.provider);
AppLogger.i('AiConfigBloc', '成功获取模型列表provider=${event.provider},模型数量=${models.length}');
// 更新缓存时间
_modelsCacheTime[event.provider] = DateTime.now();
// Use the new factory for ModelInfo list
final modelGroup = AIModelGroup.fromModelInfoList(event.provider, models);
final updatedModelGroups = Map<String, AIModelGroup>.from(state.modelGroups);
updatedModelGroups[event.provider] = modelGroup;
emit(state.copyWith(
modelsForProviderInfo: models,
modelGroups: updatedModelGroups, // Update model groups
errorMessage: () => null
));
AppLogger.i('AiConfigBloc', '模型加载完成已缓存触发GetProviderDefaultConfigprovider=${event.provider}');
add(GetProviderDefaultConfig(provider: event.provider));
} catch (e, stackTrace) {
AppLogger.e(
'AiConfigBloc', '加载模型失败 for ${event.provider}', e, stackTrace);
AppLogger.w('AiConfigBloc', '加载模型失败provider=${event.provider},错误:$e');
emit(state.copyWith(
modelsForProviderInfo: [],
errorMessage: () => '加载模型列表失败: ${e.toString()}'));
}
}
Future<void> _onAddAiConfig(
AddAiConfig event, Emitter<AiConfigState> emit) async {
emit(state.copyWith(
actionStatus: AiConfigActionStatus.loading,
actionErrorMessage: () => null));
try {
AppLogger.i('AiConfigBloc', '开始添加配置: provider=${event.provider}, modelName=${event.modelName}');
final newConfig = await _repository.addConfiguration(
userId: event.userId,
provider: event.provider,
modelName: event.modelName,
alias: event.alias,
apiKey: event.apiKey,
apiEndpoint: event.apiEndpoint,
);
AppLogger.i('AiConfigBloc', '配置添加成功: configId=${newConfig.id}');
// 直接更新列表,避免重复请求
final currentConfigs = List<UserAIModelConfigModel>.from(state.configs);
currentConfigs.add(newConfig);
// 重新构建providerDefaultConfigs
final providerDefaultConfigs = _buildProviderDefaultConfigs(currentConfigs);
// 使缓存失效,确保下次加载最新数据
_lastConfigsLoadTime = null;
emit(state.copyWith(
actionStatus: AiConfigActionStatus.success,
configs: currentConfigs,
providerDefaultConfigs: providerDefaultConfigs,
));
AppLogger.i('AiConfigBloc', '配置列表已更新,避免重复请求');
} catch (e, stackTrace) {
AppLogger.e('AiConfigBloc', '添加配置失败', e, stackTrace);
emit(state.copyWith(
actionStatus: AiConfigActionStatus.error,
actionErrorMessage: () => '添加失败: ${e.toString()}'));
}
}
Future<void> _onUpdateAiConfig(
UpdateAiConfig event, Emitter<AiConfigState> emit) async {
emit(state.copyWith(
actionStatus: AiConfigActionStatus.loading,
actionErrorMessage: () => null));
try {
final updatedConfig = await _repository.updateConfiguration(
userId: event.userId,
configId: event.configId,
alias: event.alias,
apiKey: event.apiKey,
apiEndpoint: event.apiEndpoint,
);
// 更新列表中的特定项
final currentConfigs = List<UserAIModelConfigModel>.from(state.configs);
final index = currentConfigs.indexWhere((c) => c.id == updatedConfig.id);
if (index != -1) {
currentConfigs[index] = updatedConfig;
// 重新构建providerDefaultConfigs以确保UI正确显示
final providerDefaultConfigs = _buildProviderDefaultConfigs(currentConfigs);
emit(state.copyWith(
actionStatus: AiConfigActionStatus.success,
configs: currentConfigs,
providerDefaultConfigs: providerDefaultConfigs));
} else {
// 如果找不到,最好还是重新加载
emit(state.copyWith(actionStatus: AiConfigActionStatus.success));
add(LoadAiConfigs(userId: event.userId));
}
} catch (e, stackTrace) {
AppLogger.e('AiConfigBloc', '更新配置失败', e, stackTrace);
emit(state.copyWith(
actionStatus: AiConfigActionStatus.error,
actionErrorMessage: () => '更新失败: ${e.toString()}'));
}
}
Future<void> _onDeleteAiConfig(
DeleteAiConfig event, Emitter<AiConfigState> emit) async {
emit(state.copyWith(
actionStatus: AiConfigActionStatus.loading,
actionErrorMessage: () => null));
try {
await _repository.deleteConfiguration(
userId: event.userId, configId: event.configId);
// 从列表中移除
final currentConfigs = List<UserAIModelConfigModel>.from(state.configs);
currentConfigs.removeWhere((c) => c.id == event.configId);
// 重新构建providerDefaultConfigs以确保UI正确显示
final providerDefaultConfigs = _buildProviderDefaultConfigs(currentConfigs);
emit(state.copyWith(
actionStatus: AiConfigActionStatus.success,
configs: currentConfigs,
providerDefaultConfigs: providerDefaultConfigs));
// 如果删除的是默认配置,可能需要清除默认状态或重新加载以确认新的默认(如果后端自动处理)
// 这里暂时只移除
} catch (e, stackTrace) {
AppLogger.e('AiConfigBloc', '删除配置失败', e, stackTrace);
emit(state.copyWith(
actionStatus: AiConfigActionStatus.error,
actionErrorMessage: () => '删除失败: ${e.toString()}'));
}
}
Future<void> _onValidateAiConfig(
ValidateAiConfig event, Emitter<AiConfigState> emit) async {
try {
AppLogger.i('AiConfigBloc', '开始验证配置: configId=${event.configId}');
emit(state.copyWith(
actionStatus: AiConfigActionStatus.loading,
actionErrorMessage: null,
loadingConfigId: event.configId));
final validatedConfig = await _repository.validateConfiguration(
userId: event.userId, configId: event.configId);
AppLogger.i('AiConfigBloc', '配置验证完成: configId=${event.configId}, isValidated=${validatedConfig.isValidated}');
// 更新列表中的特定项
final currentConfigs = List<UserAIModelConfigModel>.from(state.configs);
final index =
currentConfigs.indexWhere((c) => c.id == validatedConfig.id);
if (index != -1) {
currentConfigs[index] = validatedConfig;
// 重新构建providerDefaultConfigs以确保UI正确显示
final providerDefaultConfigs = _buildProviderDefaultConfigs(currentConfigs);
emit(state.copyWith(
actionStatus: AiConfigActionStatus.success,
configs: currentConfigs,
providerDefaultConfigs: providerDefaultConfigs,
loadingConfigId: null));
} else {
AppLogger.w('AiConfigBloc', '验证后找不到配置,触发重新加载');
emit(state.copyWith(
actionStatus: AiConfigActionStatus.success,
loadingConfigId: null));
add(LoadAiConfigs(userId: event.userId));
}
} catch (e, stackTrace) {
AppLogger.e('AiConfigBloc', '验证配置失败', e, stackTrace);
emit(state.copyWith(
actionStatus: AiConfigActionStatus.error,
actionErrorMessage: () => '验证请求失败: ${e.toString()}',
loadingConfigId: null));
}
}
Future<void> _onSetDefaultAiConfig(
SetDefaultAiConfig event, Emitter<AiConfigState> emit) async {
emit(state.copyWith(
actionStatus: AiConfigActionStatus.loading,
actionErrorMessage: () => null));
try {
AppLogger.i('AiConfigBloc', '开始设置默认配置: configId=${event.configId}');
final newDefaultConfig = await _repository.setDefaultConfiguration(
userId: event.userId, configId: event.configId);
// 更新所有配置的默认状态
final currentConfigs = List<UserAIModelConfigModel>.from(state.configs);
for (int i = 0; i < currentConfigs.length; i++) {
if (currentConfigs[i].id == event.configId) {
currentConfigs[i] = newDefaultConfig;
} else if (currentConfigs[i].isDefault) {
// 取消其他配置的默认状态
currentConfigs[i] = currentConfigs[i].copyWith(isDefault: false);
}
}
// 重新构建providerDefaultConfigs
final providerDefaultConfigs = _buildProviderDefaultConfigs(currentConfigs);
// 使缓存失效
_lastConfigsLoadTime = null;
emit(state.copyWith(
actionStatus: AiConfigActionStatus.success,
configs: currentConfigs,
providerDefaultConfigs: providerDefaultConfigs,
));
AppLogger.i('AiConfigBloc', '默认配置设置成功,避免重复请求');
} catch (e, stackTrace) {
AppLogger.e('AiConfigBloc', '设置默认配置失败', e, stackTrace);
emit(state.copyWith(
actionStatus: AiConfigActionStatus.error,
actionErrorMessage: () => '设置默认失败: ${e.toString()}'));
}
}
void _onClearProviderModels(
ClearProviderModels event, Emitter<AiConfigState> emit) {
// 清除模型列表和当前选中的提供商
emit(state.copyWith(
clearModels: true,
// 保留模型分组信息,因为它可能在其他地方被使用
// 如果需要清除特定提供商的模型分组,可以在这里处理
));
}
// 根据provider查找第一个可用的配置用于显示该提供商的API密钥和URL
Future<void> _onGetProviderDefaultConfig(
GetProviderDefaultConfig event, Emitter<AiConfigState> emit) async {
final provider = event.provider;
print('⚠️ 开始处理GetProviderDefaultConfig事件provider=$provider');
// 获取当前状态的providerDefaultConfigs副本
final providerDefaultConfigs = Map<String, UserAIModelConfigModel>.from(state.providerDefaultConfigs);
// 从已加载的配置中查找
final providerConfigs = state.configs.where((c) => c.provider == provider).toList();
print('⚠️ 查找provider=$provider的配置,找到${providerConfigs.length}个配置');
if (providerConfigs.isEmpty) {
print('⚠️ 没有找到provider=$provider的配置');
// 没有找到该提供商的配置从Map中移除这个提供商的配置如果有
if (providerDefaultConfigs.containsKey(provider)) {
providerDefaultConfigs.remove(provider);
emit(state.copyWith(
providerDefaultConfigs: providerDefaultConfigs,
));
print('⚠️ 已从providerDefaultConfigs中移除provider=$provider的配置');
}
return;
}
// 首先寻找默认的
final defaultConfig = providerConfigs.firstWhere(
(c) => c.isDefault,
orElse: () => providerConfigs.firstWhere(
(c) => c.isValidated,
orElse: () => providerConfigs.first,
),
);
print('⚠️ 找到provider=$provider的默认配置id=${defaultConfig.id}apiEndpoint=${defaultConfig.apiEndpoint}hasApiKey=${defaultConfig.apiKey != null}');
// 更新或添加该提供商的默认配置
providerDefaultConfigs[provider] = defaultConfig;
// 更新状态
emit(state.copyWith(
providerDefaultConfigs: providerDefaultConfigs,
));
print('⚠️ 已更新状态中的providerDefaultConfigs当前包含的提供商${providerDefaultConfigs.keys.join(", ")}');
}
// 处理加载API密钥的事件
Future<void> _onLoadApiKeyForConfig(
LoadApiKeyForConfig event, Emitter<AiConfigState> emit) async {
try {
// 从已加载的配置中查找
final config = state.configs.firstWhereOrNull(
(config) => config.id == event.configId
);
if (config != null && config.apiKey != null) {
// 如果已加载的配置中有API密钥直接使用
// event.onApiKeyLoaded(config.apiKey!); // Commenting out: ValueGetter<void> takes no arguments
print("API Key found in state for ${event.configId}");
// TODO: Decide how to actually return/use this key - maybe emit a state?
return;
}
// 如果没有找到配置或者没有API密钥提示用户手动输入
// event.onApiKeyLoaded("请手动输入API密钥"); // Commenting out: ValueGetter<void> takes no arguments
print("API Key NOT found in state for ${event.configId}");
// TODO: Decide how to handle missing key - maybe emit an error state?
} catch (e, stackTrace) {
AppLogger.e('AiConfigBloc', '获取API密钥失败', e, stackTrace);
// 如果失败,返回一个错误提示
// event.onApiKeyLoaded("获取失败,请手动输入"); // Commenting out: ValueGetter<void> takes no arguments
print("Error loading API Key for ${event.configId}: $e");
// TODO: Decide how to handle error - maybe emit an error state?
}
}
// --- Handlers for New Events ---
Future<void> _onLoadProviderCapability(
LoadProviderCapability event, Emitter<AiConfigState> emit) async {
// Reset previous capability and test status for the new provider
emit(state.copyWith(
providerCapabilityClearable: () => null,
isTestingApiKey: false,
apiKeyTestSuccessProviderClearable: () => null,
apiKeyTestErrorClearable: () => null,
));
try {
// 调用repository方法获取提供商能力
final capability = await _repository.getProviderCapability(event.providerName);
AppLogger.i('AiConfigBloc', '加载提供商 ${event.providerName} 能力成功: $capability');
emit(state.copyWith(providerCapability: capability));
// --- 修改开始 ---
// bool shouldLoadWithKey = false; // 已不再使用
UserAIModelConfigModel? defaultConfig;
// 优先从 providerDefaultConfigs 获取,因为它是为这个场景设计的
defaultConfig = state.providerDefaultConfigs[event.providerName];
// 如果默认配置里没key再尝试从完整列表里捞一个有效的 (可能不是最优选择,但作为后备)
// if (defaultConfig == null || defaultConfig.apiKey == null || defaultConfig.apiKey!.isEmpty) {
// final providerConfigs = state.configs.where((c) => c.provider == event.providerName).toList();
// if (providerConfigs.isNotEmpty) {
// defaultConfig = providerConfigs.firstWhere(
// (c) => c.isDefault && c.apiKey != null && c.apiKey!.isNotEmpty,
// orElse: () => providerConfigs.firstWhere(
// (c) => c.isValidated && c.apiKey != null && c.apiKey!.isNotEmpty,
// orElse: () => providerConfigs.firstWhere(
// (c) => c.apiKey != null && c.apiKey!.isNotEmpty,
// orElse: () => providerConfigs.first // Last resort: first config even without key
// )
// )
// );
// }
// }
if (capability == ModelListingCapability.listingWithKey) {
// 检查找到的配置(优先是 providerDefaultConfigs 里的)是否有有效的 API Key
if (defaultConfig != null && defaultConfig.apiKey != null && defaultConfig.apiKey!.isNotEmpty) {
// 注释掉自动验证逻辑避免在新建模式下自动验证API Key
// shouldLoadWithKey = true;
AppLogger.i('AiConfigBloc', 'Provider ${event.providerName} 需要 Key找到已配置的 Key但不自动验证将加载默认模型列表');
} else {
AppLogger.i('AiConfigBloc', 'Provider ${event.providerName} 需要 Key但未找到带 Key 的默认/有效配置,将加载默认模型');
}
} else {
AppLogger.i('AiConfigBloc', 'Provider ${event.providerName} 不需要 Key 或不支持列表,将加载默认模型');
}
// 清除之前的测试状态和错误信息,避免残留
emit(state.copyWith(
apiKeyTestSuccessProviderClearable: () => null,
apiKeyTestErrorClearable: () => null,
isTestingApiKey: false // 不自动测试API Key
));
// 统一使用LoadModelsForProvider加载模型列表不自动验证API Key
AppLogger.i('AiConfigBloc', '触发加载 ${event.providerName} 的默认模型列表 (LoadModelsForProvider)');
add(LoadModelsForProvider(provider: event.providerName));
// --- 修改结束 ---
} catch (e, stackTrace) {
AppLogger.e('AiConfigBloc', '加载提供商 ${event.providerName} 能力失败', e, stackTrace);
emit(state.copyWith(errorMessage: () => '加载提供商能力失败: ${e.toString()}'));
// 即使能力加载失败,也尝试加载默认模型列表,避免界面空白
AppLogger.w('AiConfigBloc', '能力加载失败,仍尝试加载 ${event.providerName} 的默认模型列表');
add(LoadModelsForProvider(provider: event.providerName));
}
}
Future<void> _onTestApiKey(
TestApiKey event, Emitter<AiConfigState> emit) async {
emit(state.copyWith(
isTestingApiKey: true,
apiKeyTestSuccessProviderClearable: () => null,
apiKeyTestErrorClearable: () => null,
));
try {
final models = await _repository.listModelsWithApiKey(
provider: event.providerName,
apiKey: event.apiKey,
apiEndpoint: event.apiEndpoint,
);
AppLogger.i('AiConfigBloc', '测试 API Key 成功 for ${event.providerName}, 获取到 ${models.length} 个模型');
// 更新缓存时间
_modelsCacheTime[event.providerName] = DateTime.now();
// Use the new factory for ModelInfo list
final modelGroup = AIModelGroup.fromModelInfoList(event.providerName, models);
final updatedModelGroups = Map<String, AIModelGroup>.from(state.modelGroups);
updatedModelGroups[event.providerName] = modelGroup;
emit(state.copyWith(
isTestingApiKey: false,
apiKeyTestSuccessProvider: event.providerName,
modelsForProviderInfo: models,
modelGroups: updatedModelGroups, // Update model groups
selectedProviderForModels: event.providerName,
));
} catch (e, stackTrace) {
AppLogger.e('AiConfigBloc', '测试 API Key 异常 for ${event.providerName}', e, stackTrace);
emit(state.copyWith(
isTestingApiKey: false,
apiKeyTestError: 'API Key 测试失败: ${e.toString()}',
modelsForProviderInfo: [],
));
}
}
// Handler to clear the API key test error
void _onClearApiKeyTestError(
ClearApiKeyTestError event, Emitter<AiConfigState> emit) {
// Use ValueGetter to explicitly set the error to null
emit(state.copyWith(apiKeyTestErrorClearable: () => null));
}
// Optional: Modify _onLoadModelsForProvider if needed
// Example: Reset API key test status when models are loaded without a key test
// Future<void> _onLoadModelsForProvider(
// LoadModelsForProvider event, Emitter<AiConfigState> emit) async {
// emit(state.copyWith(
// modelsForProvider: [],
// selectedProviderForModels: event.provider,
// // Reset test status if loading models without key
// apiKeyTestSuccessProviderClearable: () => null,
// apiKeyTestErrorClearable: () => null
// ));
// // ... rest of the existing logic ...
// }
Future<void> _onAddCustomModelAndValidate(
AddCustomModelAndValidate event, Emitter<AiConfigState> emit) async {
emit(state.copyWith(
actionStatus: AiConfigActionStatus.loading,
actionErrorMessage: () => null));
try {
AppLogger.i('AiConfigBloc', '开始添加自定义模型并验证: provider=${event.provider}, modelName=${event.modelName}');
// 首先添加配置
final newConfig = await _repository.addConfiguration(
userId: event.userId,
provider: event.provider,
modelName: event.modelName,
alias: event.alias,
apiKey: event.apiKey,
apiEndpoint: event.apiEndpoint,
);
AppLogger.i('AiConfigBloc', '自定义模型添加成功: configId=${newConfig.id}');
// 立即验证配置
try {
final validatedConfig = await _repository.validateConfiguration(
userId: event.userId,
configId: newConfig.id,
);
AppLogger.i('AiConfigBloc', '自定义模型验证完成: configId=${newConfig.id}, isValidated=${validatedConfig.isValidated}');
// 直接更新列表,避免重复请求
final currentConfigs = List<UserAIModelConfigModel>.from(state.configs);
currentConfigs.add(validatedConfig);
// 重新构建providerDefaultConfigs
final providerDefaultConfigs = _buildProviderDefaultConfigs(currentConfigs);
// 使缓存失效,确保下次加载最新数据
_lastConfigsLoadTime = null;
emit(state.copyWith(
actionStatus: AiConfigActionStatus.success,
configs: currentConfigs,
providerDefaultConfigs: providerDefaultConfigs,
));
AppLogger.i('AiConfigBloc', '自定义模型添加和验证完成,列表已更新');
} catch (validateError) {
AppLogger.w('AiConfigBloc', '自定义模型验证失败,但配置已添加: ${validateError.toString()}');
// 验证失败,但配置已添加,仍然更新列表
final currentConfigs = List<UserAIModelConfigModel>.from(state.configs);
currentConfigs.add(newConfig);
final providerDefaultConfigs = _buildProviderDefaultConfigs(currentConfigs);
_lastConfigsLoadTime = null;
emit(state.copyWith(
actionStatus: AiConfigActionStatus.success,
configs: currentConfigs,
providerDefaultConfigs: providerDefaultConfigs,
));
}
} catch (e, stackTrace) {
AppLogger.e('AiConfigBloc', '添加自定义模型失败', e, stackTrace);
emit(state.copyWith(
actionStatus: AiConfigActionStatus.error,
actionErrorMessage: () => '添加自定义模型失败: ${e.toString()}'));
}
}
void _onClearModelsCache(ClearModelsCache event, Emitter<AiConfigState> emit) {
if (event.provider != null) {
// 清除特定提供商的缓存
_modelsCacheTime.remove(event.provider);
AppLogger.i('AiConfigBloc', '已清除提供商 ${event.provider} 的模型缓存');
} else {
// 清除所有模型缓存
_modelsCacheTime.clear();
AppLogger.i('AiConfigBloc', '已清除所有模型缓存');
}
}
}

View File

@@ -0,0 +1,189 @@
part of 'ai_config_bloc.dart';
abstract class AiConfigEvent extends Equatable {
const AiConfigEvent();
@override
List<Object?> get props => [];
}
/// 加载所有配置
class LoadAiConfigs extends AiConfigEvent {
// 实际应用中应从认证状态获取
final String userId;
const LoadAiConfigs({required this.userId});
@override
List<Object?> get props => [userId];
}
/// 加载可用提供商
class LoadAvailableProviders extends AiConfigEvent {
const LoadAvailableProviders();
}
/// 加载指定提供商的模型
class LoadModelsForProvider extends AiConfigEvent {
final String provider;
const LoadModelsForProvider({required this.provider});
@override
List<Object?> get props => [provider];
}
/// 添加配置
class AddAiConfig extends AiConfigEvent {
final String userId;
final String provider;
final String modelName;
final String apiKey;
final String? alias;
final String? apiEndpoint;
const AddAiConfig({
required this.userId,
required this.provider,
required this.modelName,
required this.apiKey,
this.alias,
this.apiEndpoint,
});
@override
List<Object?> get props => [userId, provider, modelName, apiKey, alias, apiEndpoint];
}
/// 更新配置
class UpdateAiConfig extends AiConfigEvent {
final String userId;
final String configId;
final String? alias;
final String? apiKey;
final String? apiEndpoint;
const UpdateAiConfig({
required this.userId,
required this.configId,
this.alias,
this.apiKey,
this.apiEndpoint,
});
@override
List<Object?> get props => [userId, configId, alias, apiKey, apiEndpoint];
}
/// 删除配置
class DeleteAiConfig extends AiConfigEvent {
final String userId;
final String configId;
const DeleteAiConfig({required this.userId, required this.configId});
@override
List<Object?> get props => [userId, configId];
}
/// 验证配置
class ValidateAiConfig extends AiConfigEvent {
final String userId;
final String configId;
const ValidateAiConfig({required this.userId, required this.configId});
@override
List<Object?> get props => [userId, configId];
}
/// 设置默认配置
class SetDefaultAiConfig extends AiConfigEvent {
final String userId;
final String configId;
const SetDefaultAiConfig({required this.userId, required this.configId});
@override
List<Object?> get props => [userId, configId];
}
/// 清除提供商/模型列表(例如,关闭对话框时)
class ClearProviderModels extends AiConfigEvent {
const ClearProviderModels();
}
/// 获取提供商默认配置
class GetProviderDefaultConfig extends AiConfigEvent {
final String provider;
const GetProviderDefaultConfig({required this.provider});
@override
List<Object?> get props => [provider];
}
/// 加载指定配置的API密钥
class LoadApiKeyForConfig extends AiConfigEvent {
final String configId;
final ValueGetter<void> onApiKeyLoaded; // Callback to return the key
const LoadApiKeyForConfig({required this.configId, required this.onApiKeyLoaded});
@override
List<Object?> get props => [configId];
}
// --- New Events for Dynamic Loading & Validation ---
// Event to fetch the capability of a specific provider
class LoadProviderCapability extends AiConfigEvent {
final String providerName;
const LoadProviderCapability({required this.providerName});
@override
List<Object?> get props => [providerName];
}
// Event to test the API key for a specific provider
class TestApiKey extends AiConfigEvent {
final String providerName;
final String apiKey;
final String? apiEndpoint;
const TestApiKey({
required this.providerName,
required this.apiKey,
this.apiEndpoint,
});
@override
List<Object?> get props => [providerName, apiKey, apiEndpoint];
}
/// 清除API密钥测试错误状态
class ClearApiKeyTestError extends AiConfigEvent {
const ClearApiKeyTestError();
}
/// 清除模型列表缓存
class ClearModelsCache extends AiConfigEvent {
final String? provider; // 如果为null则清除所有缓存
const ClearModelsCache({this.provider});
@override
List<Object?> get props => [provider];
}
/// 添加自定义模型并立即验证
class AddCustomModelAndValidate extends AiConfigEvent {
final String userId;
final String provider;
final String modelName;
final String apiKey;
final String? alias;
final String? apiEndpoint;
const AddCustomModelAndValidate({
required this.userId,
required this.provider,
required this.modelName,
required this.apiKey,
this.alias,
this.apiEndpoint,
});
@override
List<Object?> get props => [userId, provider, modelName, apiKey, alias, apiEndpoint];
}
/// 重置AI配置状态与缓存用于登出/切换账号)
class ResetAiConfigs extends AiConfigEvent {
const ResetAiConfigs();
}

View File

@@ -0,0 +1,161 @@
part of 'ai_config_bloc.dart';
// 枚举来定义 Provider 获取模型列表的能力
enum ModelListingCapability {
noListing, // 不支持 API 获取
listingWithoutKey, // 无需 Key 获取
listingWithKey, // 需要 Key 获取
}
enum AiConfigStatus {
initial,
loading,
loaded,
error,
}
enum AiConfigActionStatus {
idle, // 初始状态
loading, // 操作进行中(例如保存、删除、验证)
success, // 操作成功
error // 操作失败
}
class AiConfigState extends Equatable {
const AiConfigState({
this.status = AiConfigStatus.initial,
this.configs = const [],
this.availableProviders = const [],
this.modelsForProvider = const [],
this.modelsForProviderInfo = const [],
this.modelGroups = const {},
this.selectedProviderForModels,
this.providerDefaultConfigs = const {},
this.loadingConfigId,
this.actionStatus = AiConfigActionStatus.idle,
this.errorMessage,
this.actionErrorMessage,
// New state fields
this.providerCapability,
this.isTestingApiKey = false,
this.apiKeyTestSuccessProvider,
this.apiKeyTestError,
});
final AiConfigStatus status;
final List<UserAIModelConfigModel> configs;
final List<String> availableProviders;
final List<String> modelsForProvider; // For the currently selected provider
final List<ModelInfo> modelsForProviderInfo; // New field for ModelInfo
final Map<String, AIModelGroup> modelGroups; // Models grouped by provider
final String? selectedProviderForModels; // Tracks which provider `modelsForProvider` belongs to
final Map<String, UserAIModelConfigModel> providerDefaultConfigs; // Provider name -> one representative config
final String? loadingConfigId; // ID of the config being validated
final AiConfigActionStatus actionStatus; // Status for CRUD/Action operations
final String? errorMessage; // General error message for loading etc.
final String? actionErrorMessage; // Specific error for the last action
// New state fields for dynamic loading and validation
final ModelListingCapability? providerCapability; // Capability of the selected provider
final bool isTestingApiKey; // Is an API key currently being tested?
final String? apiKeyTestSuccessProvider; // Which provider's key was successfully tested?
final String? apiKeyTestError; // Error message from the last API key test
// 获取已验证的配置,用于选择器
List<UserAIModelConfigModel> get validatedConfigs =>
configs.where((c) => c.isValidated).toList();
// 获取默认配置
UserAIModelConfigModel? get defaultConfig =>
configs.firstWhereOrNull((c) => c.isDefault);
// 获取特定提供商的默认配置
UserAIModelConfigModel? getProviderDefaultConfig(String provider) {
return providerDefaultConfigs[provider];
}
AiConfigState copyWith({
AiConfigStatus? status,
List<UserAIModelConfigModel>? configs,
List<String>? availableProviders,
List<String>? modelsForProvider,
List<ModelInfo>? modelsForProviderInfo,
Map<String, AIModelGroup>? modelGroups,
String? selectedProviderForModels,
// Use ValueGetter to allow clearing the value by passing () => null
ValueGetter<String?>? selectedProviderForModelsClearable,
Map<String, UserAIModelConfigModel>? providerDefaultConfigs,
String? loadingConfigId,
// Use ValueGetter for nullable loadingConfigId
ValueGetter<String?>? loadingConfigIdClearable,
AiConfigActionStatus? actionStatus,
ValueGetter<String?>? errorMessage, // Use ValueGetter for nullable fields
ValueGetter<String?>? actionErrorMessage,
// New fields
ModelListingCapability? providerCapability,
ValueGetter<ModelListingCapability?>? providerCapabilityClearable,
bool? isTestingApiKey,
String? apiKeyTestSuccessProvider,
ValueGetter<String?>? apiKeyTestSuccessProviderClearable,
String? apiKeyTestError,
ValueGetter<String?>? apiKeyTestErrorClearable,
// Helper for clearing models - not a direct state field
bool clearModels = false,
}) {
return AiConfigState(
status: status ?? this.status,
configs: configs ?? this.configs,
availableProviders: availableProviders ?? this.availableProviders,
modelsForProvider:
clearModels ? [] : (modelsForProvider ?? this.modelsForProvider),
modelsForProviderInfo:
clearModels ? [] : (modelsForProviderInfo ?? this.modelsForProviderInfo),
modelGroups: modelGroups ?? this.modelGroups,
selectedProviderForModels:
selectedProviderForModelsClearable != null
? selectedProviderForModelsClearable()
: selectedProviderForModels ?? this.selectedProviderForModels,
providerDefaultConfigs:
providerDefaultConfigs ?? this.providerDefaultConfigs,
loadingConfigId: loadingConfigIdClearable != null
? loadingConfigIdClearable()
: loadingConfigId ?? this.loadingConfigId,
actionStatus: actionStatus ?? this.actionStatus,
errorMessage: errorMessage != null ? errorMessage() : this.errorMessage,
actionErrorMessage:
actionErrorMessage != null ? actionErrorMessage() : this.actionErrorMessage,
// New fields
providerCapability: providerCapabilityClearable != null
? providerCapabilityClearable()
: providerCapability ?? this.providerCapability,
isTestingApiKey: isTestingApiKey ?? this.isTestingApiKey,
apiKeyTestSuccessProvider: apiKeyTestSuccessProviderClearable != null
? apiKeyTestSuccessProviderClearable()
: apiKeyTestSuccessProvider ?? this.apiKeyTestSuccessProvider,
apiKeyTestError: apiKeyTestErrorClearable != null
? apiKeyTestErrorClearable()
: apiKeyTestError ?? this.apiKeyTestError,
);
}
@override
List<Object?> get props => [
status,
configs,
availableProviders,
modelsForProvider,
modelsForProviderInfo,
modelGroups,
selectedProviderForModels,
providerDefaultConfigs,
loadingConfigId,
actionStatus,
errorMessage,
actionErrorMessage,
// New state fields
providerCapability,
isTestingApiKey,
apiKeyTestSuccessProvider,
apiKeyTestError,
];
}

View File

@@ -0,0 +1,100 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:ainoval/models/novel_structure.dart'; // Changed from novel_chapter.dart
import 'package:ainoval/models/novel_setting_item.dart';
import 'package:ainoval/services/api_service/repositories/editor_repository.dart'; // Changed from novel_chapter_repository.dart
import 'package:ainoval/services/api_service/repositories/novel_ai_repository.dart'; // New repository for AI features
import 'package:ainoval/utils/logger.dart';
part 'ai_setting_generation_event.dart';
part 'ai_setting_generation_state.dart';
class AISettingGenerationBloc extends Bloc<AISettingGenerationEvent, AISettingGenerationState> {
final EditorRepository _editorRepository; // Changed
final NovelAIRepository _novelAIRepository;
List<Chapter> _loadedChapters = []; // Changed from NovelChapter
AISettingGenerationBloc({
required EditorRepository editorRepository, // Changed
required NovelAIRepository novelAIRepository,
}) : _editorRepository = editorRepository, // Changed
_novelAIRepository = novelAIRepository,
super(AISettingGenerationInitial()) {
on<LoadInitialDataForAISettingPanel>(_onLoadInitialData);
on<GenerateSettingsRequested>(_onGenerateSettingsRequested);
// on<AdoptGeneratedSetting>(_onAdoptGeneratedSetting); // For later
}
Future<void> _onLoadInitialData(
LoadInitialDataForAISettingPanel event,
Emitter<AISettingGenerationState> emit,
) async {
emit(AISettingGenerationLoadingChapters());
try {
final novel = await _editorRepository.getNovelWithAllScenes(event.novelId); // Use existing method that loads all structure
if (novel != null) {
_loadedChapters = novel.acts.expand((act) => act.chapters).toList();
// Sort chapters by their order, assuming Act and Chapter orders are set
_loadedChapters.sort((a, b) {
// Find act orders first
final actA = novel.acts.firstWhere((act) => act.chapters.contains(a));
final actB = novel.acts.firstWhere((act) => act.chapters.contains(b));
if (actA.order != actB.order) {
return actA.order.compareTo(actB.order);
}
return a.order.compareTo(b.order);
});
emit(AISettingGenerationDataLoaded(chapters: _loadedChapters, novel: novel));
} else {
AppLogger.e('AISettingGenerationBloc', 'Novel not found: ${event.novelId}');
emit(AISettingGenerationFailure(error: '小说未找到', chapters: [], novel: null));
}
} catch (e, stackTrace) {
AppLogger.e('AISettingGenerationBloc', 'Error loading chapters for AI Panel', e, stackTrace);
emit(AISettingGenerationFailure(error: '加载章节列表失败: ${e.toString()}', chapters: [], novel: null));
}
}
Future<void> _onGenerateSettingsRequested(
GenerateSettingsRequested event,
Emitter<AISettingGenerationState> emit,
) async {
final currentChapters = _loadedChapters;
emit(AISettingGenerationInProgress());
try {
final settings = await _novelAIRepository.generateNovelSettings(
novelId: event.novelId,
startChapterId: event.startChapterId,
endChapterId: event.endChapterId,
settingTypes: event.settingTypes,
maxSettingsPerType: event.maxSettingsPerType,
additionalInstructions: event.additionalInstructions,
);
// 保持当前的Novel引用
final currentNovel = (state is AISettingGenerationDataLoaded) ? (state as AISettingGenerationDataLoaded).novel : null;
emit(AISettingGenerationSuccess(generatedSettings: settings, chapters: currentChapters, novel: currentNovel));
} catch (e, stackTrace) {
AppLogger.e('AISettingGenerationBloc', 'Error generating settings', e, stackTrace);
final currentNovel = (state is AISettingGenerationDataLoaded) ? (state as AISettingGenerationDataLoaded).novel : null;
emit(AISettingGenerationFailure(error: '生成设定失败: ${e.toString()}', chapters: currentChapters, novel: currentNovel));
}
}
// Future<void> _onAdoptGeneratedSetting(
// AdoptGeneratedSetting event,
// Emitter<AISettingGenerationState> emit,
// ) async {
// // This will interact with SettingBloc or its repository
// // For now, just log. Will require careful state management
// AppLogger.i('AISettingGenerationBloc', 'Adopting setting: ${event.settingItem.name} to group ${event.targetGroupId}');
// // Potentially re-emit current success state or a new state indicating adoption is in progress/done
// if (state is AISettingGenerationSuccess) {
// emit(AISettingGenerationSuccess(
// generatedSettings: (state as AISettingGenerationSuccess).generatedSettings.where((s) => s.id != event.settingItem.id).toList(), // Example: remove adopted item
// chapters: _loadedChapters,
// ));
// }
// }
}

View File

@@ -0,0 +1,60 @@
part of 'ai_setting_generation_bloc.dart';
abstract class AISettingGenerationEvent extends Equatable {
const AISettingGenerationEvent();
@override
List<Object?> get props => [];
}
class LoadInitialDataForAISettingPanel extends AISettingGenerationEvent {
final String novelId;
const LoadInitialDataForAISettingPanel(this.novelId);
@override
List<Object> get props => [novelId];
}
class GenerateSettingsRequested extends AISettingGenerationEvent {
final String novelId;
final String startChapterId;
final String? endChapterId;
final List<String> settingTypes; // Values from SettingType enum
final int maxSettingsPerType;
final String additionalInstructions;
const GenerateSettingsRequested({
required this.novelId,
required this.startChapterId,
this.endChapterId,
required this.settingTypes,
required this.maxSettingsPerType,
required this.additionalInstructions,
});
@override
List<Object?> get props => [
novelId,
startChapterId,
endChapterId,
settingTypes,
maxSettingsPerType,
additionalInstructions,
];
}
// Event for when user wants to adopt a setting (to be implemented fully later)
class AdoptGeneratedSetting extends AISettingGenerationEvent {
final NovelSettingItem settingItem;
final String targetGroupId; // ID of the SettingGroup to add to
final String novelId;
const AdoptGeneratedSetting({
required this.settingItem,
required this.targetGroupId,
required this.novelId,
});
@override
List<Object> get props => [settingItem, targetGroupId, novelId];
}

View File

@@ -0,0 +1,56 @@
part of 'ai_setting_generation_bloc.dart';
abstract class AISettingGenerationState extends Equatable {
const AISettingGenerationState();
@override
List<Object?> get props => [];
}
class AISettingGenerationInitial extends AISettingGenerationState {}
class AISettingGenerationLoadingChapters extends AISettingGenerationState {}
class AISettingGenerationDataLoaded extends AISettingGenerationState {
final List<Chapter> chapters;
final Novel? novel; // 添加Novel信息以获取Act数据
// availableSettingTypes are derived from SettingType enum directly in UI
// User selections are managed by the UI state for now.
const AISettingGenerationDataLoaded({required this.chapters, this.novel});
@override
List<Object?> get props => [chapters, novel];
}
class AISettingGenerationInProgress extends AISettingGenerationState {}
class AISettingGenerationSuccess extends AISettingGenerationState {
final List<NovelSettingItem> generatedSettings;
final List<Chapter> chapters; // Keep chapters loaded
final Novel? novel; // 添加Novel信息
const AISettingGenerationSuccess({
required this.generatedSettings,
required this.chapters,
this.novel,
});
@override
List<Object?> get props => [generatedSettings, chapters, novel];
}
class AISettingGenerationFailure extends AISettingGenerationState {
final String error;
final List<Chapter> chapters; // Keep chapters loaded if available
final Novel? novel; // 添加Novel信息
const AISettingGenerationFailure({
required this.error,
required this.chapters,
this.novel,
});
@override
List<Object?> get props => [error, chapters, novel];
}

View File

@@ -0,0 +1,529 @@
import 'dart:async';
import 'package:ainoval/services/auth_service.dart' as auth_service;
import 'package:ainoval/utils/logger.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// 认证事件
abstract class AuthEvent extends Equatable {
const AuthEvent();
@override
List<Object?> get props => [];
}
// 初始化认证事件
class AuthInitialize extends AuthEvent {}
// 登录事件
class AuthLogin extends AuthEvent {
const AuthLogin({required this.username, required this.password});
final String username;
final String password;
@override
List<Object?> get props => [username, password];
}
// 注册事件
class AuthRegister extends AuthEvent {
const AuthRegister({
required this.username,
required this.password,
this.email,
this.phone,
this.displayName,
this.captchaId,
this.captchaCode,
this.emailVerificationCode,
this.phoneVerificationCode,
});
final String username;
final String password;
final String? email;
final String? phone;
final String? displayName;
final String? captchaId;
final String? captchaCode;
final String? emailVerificationCode;
final String? phoneVerificationCode;
@override
List<Object?> get props => [username, password, email, phone, displayName, captchaId, captchaCode, emailVerificationCode, phoneVerificationCode];
}
// 手机号登录事件
class PhoneLogin extends AuthEvent {
const PhoneLogin({
required this.phone,
required this.verificationCode,
});
final String phone;
final String verificationCode;
@override
List<Object?> get props => [phone, verificationCode];
}
// 邮箱登录事件
class EmailLogin extends AuthEvent {
const EmailLogin({
required this.email,
required this.verificationCode,
});
final String email;
final String verificationCode;
@override
List<Object?> get props => [email, verificationCode];
}
// 发送验证码事件(登录时使用)
class SendVerificationCode extends AuthEvent {
const SendVerificationCode({
required this.type,
required this.target,
required this.purpose,
});
final String type; // phone or email
final String target; // phone number or email address
final String purpose; // login or register
@override
List<Object?> get props => [type, target, purpose];
}
// 发送验证码事件(注册时使用,需要图片验证码)
class SendVerificationCodeWithCaptcha extends AuthEvent {
const SendVerificationCodeWithCaptcha({
required this.type,
required this.target,
required this.purpose,
required this.captchaId,
required this.captchaCode,
});
final String type; // phone or email
final String target; // phone number or email address
final String purpose; // register
final String captchaId; // captcha id
final String captchaCode; // captcha code
@override
List<Object?> get props => [type, target, purpose, captchaId, captchaCode];
}
// 加载图片验证码事件
class LoadCaptcha extends AuthEvent {}
// 登出事件
class AuthLogout extends AuthEvent {}
// AuthService状态变化事件
class AuthServiceStateChanged extends AuthEvent {
const AuthServiceStateChanged(this.authState);
final auth_service.AuthState authState;
@override
List<Object?> get props => [authState];
}
// 认证状态
abstract class AuthState extends Equatable {
const AuthState();
@override
List<Object?> get props => [];
}
// 初始状态
class AuthInitial extends AuthState {
const AuthInitial();
@override
List<Object?> get props => [];
}
// 认证中状态
class AuthLoading extends AuthState {
const AuthLoading();
@override
List<Object?> get props => [];
}
// 已认证状态
class AuthAuthenticated extends AuthState {
const AuthAuthenticated({required this.userId, required this.username});
final String userId;
final String username;
@override
List<Object?> get props => [userId, username];
}
// 未认证状态
class AuthUnauthenticated extends AuthState {
const AuthUnauthenticated();
@override
List<Object?> get props => [];
}
// 认证错误状态
class AuthError extends AuthState {
const AuthError({required this.message});
final String message;
@override
List<Object?> get props => [message];
}
// 图片验证码加载完成状态
class CaptchaLoaded extends AuthState {
const CaptchaLoaded({
required this.captchaId,
required this.captchaImage,
});
final String captchaId;
final String captchaImage;
@override
List<Object?> get props => [captchaId, captchaImage];
}
// 验证码发送成功状态
class VerificationCodeSent extends AuthState {
const VerificationCodeSent({this.message = '验证码已发送'});
final String message;
@override
List<Object?> get props => [message];
}
// 认证Bloc
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc({required auth_service.AuthService authService})
: _authService = authService,
super(const AuthInitial()) {
on<AuthInitialize>(_onInitialize);
on<AuthLogin>(_onLogin);
on<AuthRegister>(_onRegister);
on<AuthLogout>(_onLogout);
on<PhoneLogin>(_onPhoneLogin);
on<EmailLogin>(_onEmailLogin);
on<SendVerificationCode>(_onSendVerificationCode);
on<SendVerificationCodeWithCaptcha>(_onSendVerificationCodeWithCaptcha);
on<LoadCaptcha>(_onLoadCaptcha);
on<AuthServiceStateChanged>(_onAuthServiceStateChanged);
// 监听认证服务的状态变化
_authStateSubscription = _authService.authStateStream.listen((authState) {
add(AuthServiceStateChanged(authState));
});
}
final auth_service.AuthService _authService;
StreamSubscription? _authStateSubscription;
Future<void> _onInitialize(AuthInitialize event, Emitter<AuthState> emit) async {
final currentState = _authService.currentState;
if (currentState.isAuthenticated) {
emit(AuthAuthenticated(
userId: currentState.userId,
username: currentState.username,
));
} else {
emit(AuthUnauthenticated());
}
}
Future<void> _onLogin(AuthLogin event, Emitter<AuthState> emit) async {
emit(const AuthLoading());
try {
final result = await _authService.login(event.username, event.password);
if (result.isAuthenticated) {
emit(AuthAuthenticated(
userId: result.userId,
username: result.username,
));
} else {
emit(AuthError(message: result.error ?? '登录失败'));
}
} catch (e) {
// 优先使用后端返回的错误信息
emit(AuthError(message: e.toString().replaceFirst('AuthException: ', '')));
}
}
Future<void> _onRegister(AuthRegister event, Emitter<AuthState> emit) async {
emit(const AuthLoading());
try {
final bool needVerification = (event.email != null && event.email!.isNotEmpty) ||
(event.phone != null && event.phone!.isNotEmpty) ||
(event.captchaId != null && event.captchaId!.isNotEmpty) ||
(event.captchaCode != null && event.captchaCode!.isNotEmpty) ||
(event.emailVerificationCode != null && event.emailVerificationCode!.isNotEmpty) ||
(event.phoneVerificationCode != null && event.phoneVerificationCode!.isNotEmpty);
final auth_service.AuthState result = needVerification
? await _authService.registerWithVerification(
username: event.username,
password: event.password,
email: event.email,
phone: event.phone,
displayName: event.displayName,
captchaId: event.captchaId,
captchaCode: event.captchaCode,
emailVerificationCode: event.emailVerificationCode,
phoneVerificationCode: event.phoneVerificationCode,
)
: await _authService.registerQuick(
username: event.username,
password: event.password,
displayName: event.displayName,
);
if (result.isAuthenticated) {
emit(AuthAuthenticated(
userId: result.userId,
username: result.username,
));
} else {
emit(AuthError(message: result.error ?? '注册失败'));
}
} catch (e) {
emit(AuthError(message: e.toString().replaceFirst('AuthException: ', '')));
}
}
Future<void> _onLogout(AuthLogout event, Emitter<AuthState> emit) async {
AppLogger.i('AuthBloc', '开始执行退出登录');
emit(const AuthLoading());
try {
// 调用AuthService清除认证状态但不等待完成
_authService.logout().catchError((e) {
AppLogger.w('AuthBloc', 'AuthService.logout()执行出错但不影响BLoC状态', e);
});
// 立即发出未认证状态确保UI快速响应
AppLogger.i('AuthBloc', '发出AuthUnauthenticated状态');
const unauthenticatedState = AuthUnauthenticated();
AppLogger.i('AuthBloc', '准备emit状态: ${unauthenticatedState.runtimeType} - ${unauthenticatedState.hashCode}');
emit(unauthenticatedState);
AppLogger.i('AuthBloc', '已emit AuthUnauthenticated状态当前BLoC状态: ${state.runtimeType}');
} catch (e) {
// 即使出现任何错误,都要确保用户退出到登录页面
AppLogger.w('AuthBloc', '退出登录过程中出现错误,强制设为未认证状态', e);
emit(const AuthUnauthenticated());
}
}
Future<void> _onPhoneLogin(PhoneLogin event, Emitter<AuthState> emit) async {
emit(const AuthLoading());
try {
final result = await _authService.loginWithPhone(
phone: event.phone,
verificationCode: event.verificationCode,
);
if (result.isAuthenticated) {
emit(AuthAuthenticated(
userId: result.userId,
username: result.username,
));
} else {
emit(AuthError(message: result.error ?? '登录失败'));
}
} catch (e) {
emit(AuthError(message: e.toString().replaceFirst('AuthException: ', '')));
}
}
Future<void> _onEmailLogin(EmailLogin event, Emitter<AuthState> emit) async {
emit(const AuthLoading());
try {
final result = await _authService.loginWithEmail(
email: event.email,
verificationCode: event.verificationCode,
);
if (result.isAuthenticated) {
emit(AuthAuthenticated(
userId: result.userId,
username: result.username,
));
} else {
emit(AuthError(message: result.error ?? '登录失败'));
}
} catch (e) {
emit(AuthError(message: e.toString().replaceFirst('AuthException: ', '')));
}
}
Future<void> _onSendVerificationCode(SendVerificationCode event, Emitter<AuthState> emit) async {
try {
final success = await _authService.sendVerificationCode(
type: event.type,
target: event.target,
purpose: event.purpose,
);
if (success) {
emit(VerificationCodeSent());
// 不需要调用AuthInitialize避免重置认证状态
} else {
emit(const AuthError(message: '验证码发送失败,请稍后重试'));
}
} catch (e) {
emit(AuthError(message: _formatUserFriendlyError(e)));
}
}
Future<void> _onSendVerificationCodeWithCaptcha(SendVerificationCodeWithCaptcha event, Emitter<AuthState> emit) async {
try {
// 先验证图片验证码是否填写
if (event.captchaCode.isEmpty) {
emit(const AuthError(message: '请输入图片验证码'));
return;
}
final success = await _authService.sendVerificationCodeWithCaptcha(
type: event.type,
target: event.target,
purpose: event.purpose,
captchaId: event.captchaId,
captchaCode: event.captchaCode,
);
if (success) {
emit(VerificationCodeSent(message: '验证码已发送,请查收'));
// 验证码发送成功后,保持当前的图片验证码
// 用户注册时将使用相同的图片验证码ID和内容
await Future.delayed(const Duration(milliseconds: 100));
// 返回到图片验证码加载状态,但不重新加载(保持一致性)
if (state is CaptchaLoaded) {
final currentState = state as CaptchaLoaded;
emit(CaptchaLoaded(
captchaId: currentState.captchaId,
captchaImage: currentState.captchaImage,
));
}
} else {
emit(const AuthError(message: '验证码发送失败,请稍后重试'));
}
} catch (e) {
final errorMessage = e.toString().contains('图片验证码')
? e.toString().replaceFirst('Exception: ', '')
: '验证码发送失败:${_formatUserFriendlyError(e)}';
emit(AuthError(message: errorMessage));
// 验证失败时重新加载图片验证码
add(LoadCaptcha());
}
}
Future<void> _onLoadCaptcha(LoadCaptcha event, Emitter<AuthState> emit) async {
try {
final captchaData = await _authService.loadCaptcha();
if (captchaData != null) {
emit(CaptchaLoaded(
captchaId: captchaData['captchaId'] ?? '',
captchaImage: captchaData['captchaImage'] ?? '',
));
} else {
emit(const AuthError(message: '加载验证码失败'));
}
} catch (e) {
emit(AuthError(message: _formatUserFriendlyError(e)));
}
}
Future<void> _onAuthServiceStateChanged(AuthServiceStateChanged event, Emitter<AuthState> emit) async {
final authState = event.authState;
if (authState.isAuthenticated) {
emit(AuthAuthenticated(
userId: authState.userId,
username: authState.username,
));
} else if (authState.error != null) {
emit(AuthError(message: authState.error!));
} else {
emit(AuthUnauthenticated());
}
}
/// 将技术性错误转换为用户友好的错误消息
String _formatUserFriendlyError(dynamic error) {
final errorString = error.toString().toLowerCase();
// 网络相关错误
if (errorString.contains('connection') || errorString.contains('network') || errorString.contains('timeout')) {
return '网络连接失败,请检查您的网络连接后重试';
}
// 认证相关错误
if (errorString.contains('unauthorized') || errorString.contains('401') || errorString.contains('authentication')) {
return '用户名或密码错误,请重新输入';
}
// 服务器错误
if (errorString.contains('500') || errorString.contains('server') || errorString.contains('internal')) {
return '服务器暂时无法访问,请稍后重试';
}
// 验证码相关错误
if (errorString.contains('captcha') || errorString.contains('verification')) {
return '验证码错误或已过期,请重新输入';
}
// 用户不存在
if (errorString.contains('user not found') || errorString.contains('not found')) {
return '用户不存在,请检查用户名或先注册账号';
}
// 密码错误
if (errorString.contains('password') && errorString.contains('wrong')) {
return '密码错误,请重新输入正确的密码';
}
// 账号被禁用
if (errorString.contains('disabled') || errorString.contains('banned')) {
return '账号已被禁用,请联系管理员';
}
// 格式错误
if (errorString.contains('format') || errorString.contains('invalid')) {
return '输入格式不正确,请检查后重新输入';
}
// 如果是AuthException尝试提取更友好的消息
if (error.runtimeType.toString().contains('AuthException')) {
final message = error.toString();
if (message.contains('AuthException:')) {
return message.replaceAll('AuthException:', '').trim();
}
}
// 默认友好错误消息
return '登录失败,请稍后重试或联系客服';
}
@override
Future<void> close() {
_authStateSubscription?.cancel();
return super.close();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,177 @@
import 'package:equatable/equatable.dart';
import '../../models/chat_models.dart';
import '../../models/ai_request_models.dart';
abstract class ChatEvent extends Equatable {
const ChatEvent();
@override
List<Object?> get props => [];
}
// 加载聊天会话列表
class LoadChatSessions extends ChatEvent {
const LoadChatSessions({required this.novelId});
final String novelId;
@override
List<Object?> get props => [novelId];
}
// 创建新的聊天会话
class CreateChatSession extends ChatEvent {
const CreateChatSession({
required this.title,
required this.novelId,
this.chapterId,
this.metadata,
});
final String title;
final String novelId;
final String? chapterId;
final Map<String, dynamic>? metadata;
@override
List<Object?> get props => [title, novelId, chapterId];
}
// 选择聊天会话
class SelectChatSession extends ChatEvent {
const SelectChatSession({required this.sessionId, this.novelId});
final String sessionId;
final String? novelId;
@override
List<Object?> get props => [sessionId, novelId];
}
// 发送消息
class SendMessage extends ChatEvent {
// <<< Add configId field
// <<< Modify existing constructor
const SendMessage({required this.content, this.configId});
final String content;
final String? configId;
@override
List<Object?> get props => [content, configId]; // <<< Add configId to props
}
// 加载更多消息
class LoadMoreMessages extends ChatEvent {
const LoadMoreMessages();
}
// 更新聊天标题
class UpdateChatTitle extends ChatEvent {
const UpdateChatTitle({required this.newTitle});
final String newTitle;
@override
List<Object?> get props => [newTitle];
}
// 执行操作
class ExecuteAction extends ChatEvent {
const ExecuteAction({required this.action});
final MessageAction action;
@override
List<Object?> get props => [action];
}
// 删除聊天会话
class DeleteChatSession extends ChatEvent {
const DeleteChatSession({required this.sessionId});
final String sessionId;
@override
List<Object?> get props => [sessionId];
}
// 取消正在进行的请求
class CancelOngoingRequest extends ChatEvent {
const CancelOngoingRequest();
}
class UpdateActiveChatConfig extends ChatEvent {
const UpdateActiveChatConfig({required this.configId});
final String? configId;
@override
List<Object?> get props => [configId];
}
// 更新聊天上下文
class UpdateChatContext extends ChatEvent {
const UpdateChatContext({required this.context});
final ChatContext context;
@override
List<Object?> get props => [context];
}
// 更新聊天模型
class UpdateChatModel extends ChatEvent {
// Pass the ID, Bloc will resolve the model
const UpdateChatModel({
required this.sessionId,
required this.modelConfigId,
});
final String sessionId;
final String modelConfigId;
@override
List<Object?> get props => [sessionId, modelConfigId];
}
// 加载设定和片段数据
class LoadContextData extends ChatEvent {
const LoadContextData({required this.novelId});
final String novelId;
@override
List<Object?> get props => [novelId];
}
// 缓存设定数据
class CacheSettingsData extends ChatEvent {
const CacheSettingsData({
required this.novelId,
required this.settings,
required this.settingGroups,
});
final String novelId;
final List<dynamic> settings; // 使用dynamic避免循环导入
final List<dynamic> settingGroups;
@override
List<Object?> get props => [novelId, settings, settingGroups];
}
// 缓存片段数据
class CacheSnippetsData extends ChatEvent {
const CacheSnippetsData({
required this.novelId,
required this.snippets,
});
final String novelId;
final List<dynamic> snippets; // 使用dynamic避免循环导入
@override
List<Object?> get props => [novelId, snippets];
}
// 🚀 新增:更新聊天配置
class UpdateChatConfiguration extends ChatEvent {
const UpdateChatConfiguration({
required this.sessionId,
required this.config,
});
final String sessionId;
final UniversalAIRequest config;
@override
List<Object?> get props => [sessionId, config];
}

View File

@@ -0,0 +1,146 @@
import 'package:equatable/equatable.dart';
import '../../models/chat_models.dart';
import '../../models/user_ai_model_config_model.dart';
import '../../models/ai_request_models.dart';
abstract class ChatState extends Equatable {
const ChatState();
@override
List<Object?> get props => [];
}
// 初始状态
class ChatInitial extends ChatState {}
// 加载会话列表中
class ChatSessionsLoading extends ChatState {}
// 会话列表加载完成
class ChatSessionsLoaded extends ChatState {
const ChatSessionsLoaded({
required this.sessions,
this.error,
});
final List<ChatSession> sessions;
final String? error;
@override
List<Object?> get props => [sessions, error];
ChatSessionsLoaded copyWith({
List<ChatSession>? sessions,
String? error,
bool clearError = false,
}) {
return ChatSessionsLoaded(
sessions: sessions ?? this.sessions,
error: clearError ? null : error ?? this.error,
);
}
}
// 加载单个会话中
class ChatSessionLoading extends ChatState {}
// 会话激活状态
class ChatSessionActive extends ChatState {
const ChatSessionActive({
required this.session,
required this.context,
this.messages = const [],
this.selectedModel,
this.isGenerating = false,
this.isLoadingHistory = false,
this.error,
this.cachedSettings = const [],
this.cachedSettingGroups = const [],
this.cachedSnippets = const [],
this.isLoadingContextData = false,
this.configUpdateTimestamp,
});
final ChatSession session;
final ChatContext context;
final List<ChatMessage> messages;
final UserAIModelConfigModel? selectedModel;
final bool isGenerating;
final bool isLoadingHistory;
final String? error;
// 缓存的上下文数据
final List<dynamic> cachedSettings; // NovelSettingItem列表
final List<dynamic> cachedSettingGroups; // SettingGroup列表
final List<dynamic> cachedSnippets; // NovelSnippet列表
final bool isLoadingContextData;
final DateTime? configUpdateTimestamp; // 配置更新时间戳用于触发UI重建
@override
List<Object?> get props => [
session,
context,
messages,
selectedModel,
isGenerating,
isLoadingHistory,
error,
cachedSettings,
cachedSettingGroups,
cachedSnippets,
isLoadingContextData,
configUpdateTimestamp,
];
ChatSessionActive copyWith({
ChatSession? session,
ChatContext? context,
List<ChatMessage>? messages,
Object? selectedModel = const Object(),
bool? isGenerating,
bool? isLoadingHistory,
String? error,
bool clearError = false,
List<dynamic>? cachedSettings,
List<dynamic>? cachedSettingGroups,
List<dynamic>? cachedSnippets,
bool? isLoadingContextData,
DateTime? configUpdateTimestamp,
}) {
UserAIModelConfigModel? updatedSelectedModel;
if (selectedModel is UserAIModelConfigModel?){
updatedSelectedModel = selectedModel;
} else {
updatedSelectedModel = this.selectedModel;
}
return ChatSessionActive(
session: session ?? this.session,
context: context ?? this.context,
messages: messages ?? this.messages,
selectedModel: updatedSelectedModel,
isGenerating: isGenerating ?? this.isGenerating,
isLoadingHistory: isLoadingHistory ?? this.isLoadingHistory,
error: clearError ? null : error ?? this.error,
cachedSettings: cachedSettings ?? this.cachedSettings,
cachedSettingGroups: cachedSettingGroups ?? this.cachedSettingGroups,
cachedSnippets: cachedSnippets ?? this.cachedSnippets,
isLoadingContextData: isLoadingContextData ?? this.isLoadingContextData,
configUpdateTimestamp: configUpdateTimestamp ?? this.configUpdateTimestamp,
);
}
}
// 错误状态
class ChatError extends ChatState {
const ChatError({required this.message});
final String message;
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,65 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../models/user_credit.dart';
import '../../services/api_service/repositories/credit_repository.dart';
import '../../utils/logger.dart';
part 'credit_event.dart';
part 'credit_state.dart';
/// 用户积分BLoC
/// 负责管理用户积分状态和数据获取
class CreditBloc extends Bloc<CreditEvent, CreditState> {
final CreditRepository _repository;
static const String _tag = 'CreditBloc';
CreditBloc({required CreditRepository repository})
: _repository = repository,
super(const CreditInitial()) {
on<LoadUserCredits>(_onLoadUserCredits);
on<RefreshUserCredits>(_onRefreshUserCredits);
on<ClearCredits>(_onClearCredits);
}
/// 处理加载用户积分事件
Future<void> _onLoadUserCredits(
LoadUserCredits event,
Emitter<CreditState> emit,
) async {
emit(const CreditLoading());
await _loadCredits(emit);
}
/// 处理刷新用户积分事件
Future<void> _onRefreshUserCredits(
RefreshUserCredits event,
Emitter<CreditState> emit,
) async {
// 刷新不显示loading状态保持当前显示
await _loadCredits(emit);
}
/// 处理清空用户积分事件
Future<void> _onClearCredits(
ClearCredits event,
Emitter<CreditState> emit,
) async {
AppLogger.i(_tag, '清空用户积分状态,重置为初始状态');
emit(const CreditInitial());
}
/// 加载积分的公共方法
Future<void> _loadCredits(Emitter<CreditState> emit) async {
try {
AppLogger.i(_tag, '开始加载用户积分');
final userCredit = await _repository.getUserCredits();
AppLogger.i(_tag, '用户积分加载成功: ${userCredit.credits}');
emit(CreditLoaded(userCredit: userCredit));
} catch (e, stackTrace) {
AppLogger.e(_tag, '加载用户积分失败', e, stackTrace);
emit(CreditError(message: '加载用户积分失败: ${e.toString()}'));
}
}
}

View File

@@ -0,0 +1,24 @@
part of 'credit_bloc.dart';
/// 积分事件基类
abstract class CreditEvent extends Equatable {
const CreditEvent();
@override
List<Object?> get props => [];
}
/// 加载用户积分事件
class LoadUserCredits extends CreditEvent {
const LoadUserCredits();
}
/// 刷新用户积分事件
class RefreshUserCredits extends CreditEvent {
const RefreshUserCredits();
}
/// 清空用户积分状态事件(用于退出登录时重置为游客状态)
class ClearCredits extends CreditEvent {
const ClearCredits();
}

View File

@@ -0,0 +1,48 @@
part of 'credit_bloc.dart';
/// 积分状态基类
abstract class CreditState extends Equatable {
const CreditState();
@override
List<Object?> get props => [];
}
/// 积分初始状态
class CreditInitial extends CreditState {
const CreditInitial();
}
/// 积分加载中状态
class CreditLoading extends CreditState {
const CreditLoading();
}
/// 积分加载成功状态
class CreditLoaded extends CreditState {
final UserCredit userCredit;
const CreditLoaded({required this.userCredit});
@override
List<Object?> get props => [userCredit];
/// 创建副本
CreditLoaded copyWith({
UserCredit? userCredit,
}) {
return CreditLoaded(
userCredit: userCredit ?? this.userCredit,
);
}
}
/// 积分加载失败状态
class CreditError extends CreditState {
final String message;
const CreditError({required this.message});
@override
List<Object?> get props => [message];
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,585 @@
part of 'editor_bloc.dart';
abstract class EditorEvent extends Equatable {
const EditorEvent();
@override
List<Object?> get props => [];
}
// 🚀 新增Plan视图模式切换事件
class SwitchToPlanView extends EditorEvent {
const SwitchToPlanView();
}
class SwitchToWriteView extends EditorEvent {
const SwitchToWriteView();
}
// 🚀 新增Plan视图专用的加载事件加载场景摘要
class LoadPlanContent extends EditorEvent {
const LoadPlanContent();
}
// 🚀 新增Plan视图的场景移动事件
class MoveScene extends EditorEvent {
const MoveScene({
required this.novelId,
required this.sourceActId,
required this.sourceChapterId,
required this.sourceSceneId,
required this.targetActId,
required this.targetChapterId,
required this.targetIndex,
});
final String novelId;
final String sourceActId;
final String sourceChapterId;
final String sourceSceneId;
final String targetActId;
final String targetChapterId;
final int targetIndex;
@override
List<Object?> get props => [
novelId,
sourceActId,
sourceChapterId,
sourceSceneId,
targetActId,
targetChapterId,
targetIndex,
];
}
// 🚀 新增从Plan视图切换到Write视图并跳转到指定场景
class NavigateToSceneFromPlan extends EditorEvent {
const NavigateToSceneFromPlan({
required this.actId,
required this.chapterId,
required this.sceneId,
});
final String actId;
final String chapterId;
final String sceneId;
@override
List<Object?> get props => [actId, chapterId, sceneId];
}
// 🚀 新增刷新编辑器数据事件用于Plan视图数据修改后的无感刷新
class RefreshEditorData extends EditorEvent {
const RefreshEditorData({
this.preserveActiveScene = true,
this.source = 'plan_view',
});
final bool preserveActiveScene;
final String source;
@override
List<Object?> get props => [preserveActiveScene, source];
}
// 🚀 新增:沉浸模式切换事件
class SwitchToImmersiveMode extends EditorEvent {
const SwitchToImmersiveMode({
this.chapterId,
});
final String? chapterId; // 可指定沉浸的章节为null时使用当前活动章节
@override
List<Object?> get props => [chapterId];
}
class SwitchToNormalMode extends EditorEvent {
const SwitchToNormalMode();
}
// 🚀 新增:沉浸模式下的章节导航事件
class NavigateToNextChapter extends EditorEvent {
const NavigateToNextChapter();
}
class NavigateToPreviousChapter extends EditorEvent {
const NavigateToPreviousChapter();
}
/// 使用分页加载编辑器内容事件
class LoadEditorContentPaginated extends EditorEvent {
const LoadEditorContentPaginated({
required this.novelId,
this.loadAllSummaries = false,
});
final String novelId;
final bool loadAllSummaries;
@override
List<Object?> get props => [novelId, loadAllSummaries];
}
/// 加载更多场景事件
class LoadMoreScenes extends EditorEvent {
const LoadMoreScenes({
required this.fromChapterId,
required this.direction,
required this.actId,
this.chaptersLimit = 3,
this.targetChapterId,
this.targetSceneId,
this.preventFocusChange = false,
this.loadFromLocalOnly = false,
this.skipIfLoading = false,
this.skipAPIFallback = false,
});
final String fromChapterId;
final String direction; // "up" 或 "down" 或 "center"
final String actId; // 现在将actId作为必需参数
final int chaptersLimit;
final String? targetChapterId;
final String? targetSceneId;
final bool preventFocusChange;
final bool loadFromLocalOnly; // 是否只从本地加载,避免网络请求
final bool skipIfLoading; // 如果已经有加载任务,是否跳过此次加载
final bool skipAPIFallback; // 当loadFromLocalOnly为true且本地加载失败时是否跳过API回退
@override
List<Object?> get props => [
fromChapterId,
direction,
chaptersLimit,
actId,
targetChapterId,
targetSceneId,
preventFocusChange,
loadFromLocalOnly,
skipIfLoading,
skipAPIFallback,
];
}
class UpdateContent extends EditorEvent {
const UpdateContent({required this.content});
final String content;
@override
List<Object?> get props => [content];
}
class SaveContent extends EditorEvent {
const SaveContent();
}
class UpdateSceneContent extends EditorEvent {
const UpdateSceneContent({
required this.novelId,
required this.actId,
required this.chapterId,
required this.sceneId,
required this.content,
this.wordCount,
this.shouldRebuild = true,
this.isMinorChange,
});
final String novelId;
final String actId;
final String chapterId;
final String sceneId;
final String content;
final String? wordCount;
final bool shouldRebuild;
final bool? isMinorChange; // 是否为微小改动微小改动可以不刷新保存状态UI
@override
List<Object?> get props =>
[novelId, actId, chapterId, sceneId, content, wordCount, shouldRebuild, isMinorChange];
}
class UpdateSummary extends EditorEvent {
const UpdateSummary({
required this.novelId,
required this.actId,
required this.chapterId,
required this.sceneId,
required this.summary,
this.shouldRebuild = true,
});
final String novelId;
final String actId;
final String chapterId;
final String sceneId;
final String summary;
final bool shouldRebuild;
@override
List<Object?> get props =>
[novelId, actId, chapterId, sceneId, summary, shouldRebuild];
}
class SetActiveChapter extends EditorEvent {
const SetActiveChapter({
required this.actId,
required this.chapterId,
this.shouldScroll = true,
this.silent = false,
});
final String actId;
final String chapterId;
final bool shouldScroll;
final bool silent;
@override
List<Object?> get props => [actId, chapterId, shouldScroll, silent];
}
class ToggleEditorSettings extends EditorEvent {
const ToggleEditorSettings();
}
class UpdateEditorSettings extends EditorEvent {
const UpdateEditorSettings({required this.settings});
final Map<String, dynamic> settings;
@override
List<Object?> get props => [settings];
}
/// 🚀 新增:加载用户编辑器设置事件
class LoadUserEditorSettings extends EditorEvent {
const LoadUserEditorSettings({required this.userId});
final String userId;
@override
List<Object?> get props => [userId];
}
class UpdateActTitle extends EditorEvent {
const UpdateActTitle({
required this.actId,
required this.title,
});
final String actId;
final String title;
@override
List<Object?> get props => [actId, title];
}
class UpdateChapterTitle extends EditorEvent {
const UpdateChapterTitle({
required this.actId,
required this.chapterId,
required this.title,
});
final String actId;
final String chapterId;
final String title;
@override
List<Object?> get props => [actId, chapterId, title];
}
// 添加新的Act事件
class AddNewAct extends EditorEvent {
const AddNewAct({this.title = '新Act'});
final String title;
@override
List<Object?> get props => [title];
}
// 添加新的Chapter事件
class AddNewChapter extends EditorEvent {
const AddNewChapter({
required this.novelId,
required this.actId,
this.title = '新章节',
});
final String novelId;
final String actId;
final String title;
@override
List<Object?> get props => [novelId, actId, title];
}
// 添加新的Scene事件
class AddNewScene extends EditorEvent {
const AddNewScene({
required this.novelId,
required this.actId,
required this.chapterId,
required this.sceneId,
});
final String novelId;
final String actId;
final String chapterId;
final String sceneId;
@override
List<Object?> get props => [novelId, actId, chapterId, sceneId];
}
// 设置活动场景事件
class SetActiveScene extends EditorEvent {
const SetActiveScene({
required this.actId,
required this.chapterId,
required this.sceneId,
this.shouldScroll = true,
this.silent = false,
});
final String actId;
final String chapterId;
final String sceneId;
final bool shouldScroll;
final bool silent;
@override
List<Object?> get props => [actId, chapterId, sceneId, shouldScroll, silent];
}
// 删除场景事件 (New Event)
class DeleteScene extends EditorEvent {
const DeleteScene({
required this.novelId,
required this.actId,
required this.chapterId,
required this.sceneId,
});
final String novelId;
final String actId;
final String chapterId;
final String sceneId;
@override
List<Object?> get props => [novelId, actId, chapterId, sceneId];
}
// 删除章节事件
class DeleteChapter extends EditorEvent {
const DeleteChapter({
required this.novelId,
required this.actId,
required this.chapterId,
});
final String novelId;
final String actId;
final String chapterId;
@override
List<Object?> get props => [novelId, actId, chapterId];
}
// 删除卷(Act)事件
class DeleteAct extends EditorEvent {
const DeleteAct({
required this.novelId,
required this.actId,
});
final String novelId;
final String actId;
@override
List<Object?> get props => [novelId, actId];
}
// 生成场景摘要事件
class GenerateSceneSummaryRequested extends EditorEvent {
final String sceneId;
final String? styleInstructions;
const GenerateSceneSummaryRequested({
required this.sceneId,
this.styleInstructions,
});
@override
List<Object?> get props => [sceneId, styleInstructions];
}
// 从摘要生成场景内容事件
class GenerateSceneFromSummaryRequested extends EditorEvent {
final String novelId;
final String summary;
final String? chapterId;
final String? styleInstructions;
final bool useStreamingMode;
const GenerateSceneFromSummaryRequested({
required this.novelId,
required this.summary,
this.chapterId,
this.styleInstructions,
this.useStreamingMode = true,
});
@override
List<Object?> get props => [novelId, summary, chapterId, styleInstructions, useStreamingMode];
}
// 更新生成的场景内容事件 (用于流式响应)
class UpdateGeneratedSceneContent extends EditorEvent {
final String content;
const UpdateGeneratedSceneContent(this.content);
@override
List<Object?> get props => [content];
}
// 完成场景生成事件
class SceneGenerationCompleted extends EditorEvent {
final String content;
const SceneGenerationCompleted(this.content);
@override
List<Object?> get props => [content];
}
// 场景生成失败事件
class SceneGenerationFailed extends EditorEvent {
final String error;
const SceneGenerationFailed(this.error);
@override
List<Object?> get props => [error];
}
// 场景摘要生成完成事件
class SceneSummaryGenerationCompleted extends EditorEvent {
final String summary;
const SceneSummaryGenerationCompleted(this.summary);
@override
List<Object?> get props => [summary];
}
// 场景摘要生成失败事件
class SceneSummaryGenerationFailed extends EditorEvent {
final String error;
const SceneSummaryGenerationFailed(this.error);
@override
List<Object?> get props => [error];
}
// 停止场景生成事件
class StopSceneGeneration extends EditorEvent {
const StopSceneGeneration();
@override
List<Object?> get props => [];
}
// 刷新编辑器事件
class RefreshEditor extends EditorEvent {
const RefreshEditor();
@override
List<Object?> get props => [];
}
// 设置待处理的摘要内容事件
class SetPendingSummary extends EditorEvent {
final String summary;
const SetPendingSummary({
required this.summary,
});
@override
List<Object?> get props => [summary];
}
/// 保存场景内容事件
class SaveSceneContent extends EditorEvent {
final String novelId;
final String actId;
final String chapterId;
final String sceneId;
final String content;
final String wordCount;
final bool localOnly; // 添加参数:是否只保存到本地
const SaveSceneContent({
required this.novelId,
required this.actId,
required this.chapterId,
required this.sceneId,
required this.content,
required this.wordCount,
this.localOnly = false, // 默认为false表示同时同步到服务器
});
@override
List<Object?> get props => [novelId, actId, chapterId, sceneId, content, wordCount, localOnly];
}
/// 强制保存场景内容事件 - 用于SceneEditor dispose时的数据保存
/// 这个事件会立即、同步地保存场景内容,不经过防抖处理
class ForceSaveSceneContent extends EditorEvent {
final String novelId;
final String actId;
final String chapterId;
final String sceneId;
final String content;
final String? wordCount;
final String? summary;
const ForceSaveSceneContent({
required this.novelId,
required this.actId,
required this.chapterId,
required this.sceneId,
required this.content,
this.wordCount,
this.summary,
});
@override
List<Object?> get props => [novelId, actId, chapterId, sceneId, content, wordCount, summary];
}
class UpdateVisibleRange extends EditorEvent {
const UpdateVisibleRange({
required this.startIndex,
required this.endIndex,
});
final int startIndex;
final int endIndex;
@override
List<Object?> get props => [startIndex, endIndex];
}
/// 重置章节加载标记
class ResetActLoadingFlags extends EditorEvent {
const ResetActLoadingFlags();
}
/// 设置章节加载边界标记
class SetActLoadingFlags extends EditorEvent {
final bool? hasReachedEnd;
final bool? hasReachedStart;
const SetActLoadingFlags({
this.hasReachedEnd,
this.hasReachedStart,
});
}
// 设置焦点章节事件
class SetFocusChapter extends EditorEvent {
const SetFocusChapter({
required this.chapterId,
});
final String chapterId;
@override
List<Object?> get props => [chapterId];
}

View File

@@ -0,0 +1,270 @@
part of 'editor_bloc.dart';
// AI生成状态
enum AIGenerationStatus {
/// 初始状态
initial,
/// 生成中
generating,
/// 生成完成
completed,
/// 生成失败
failed,
}
abstract class EditorState extends Equatable {
const EditorState();
@override
List<Object?> get props => [];
}
class EditorInitial extends EditorState {}
class EditorLoading extends EditorState {}
class EditorLoaded extends EditorState {
const EditorLoaded({
required this.novel,
required this.settings,
this.activeActId,
this.activeChapterId,
this.activeSceneId,
this.focusChapterId,
this.isDirty = false,
this.isSaving = false,
this.isLoading = false,
this.hasReachedEnd = false,
this.hasReachedStart = false,
this.lastSaveTime,
this.errorMessage,
this.aiSummaryGenerationStatus = AIGenerationStatus.initial,
this.aiSceneGenerationStatus = AIGenerationStatus.initial,
this.generatedSummary,
this.generatedSceneContent,
this.aiGenerationError,
this.isStreamingGeneration = false,
this.pendingSummary,
this.visibleRange,
this.virtualListEnabled = true,
this.chapterGlobalIndices = const {},
this.chapterToActMap = const {},
this.lastUpdateSilent = false,
this.isPlanViewMode = false,
this.planViewDirty = false,
this.lastPlanModifiedTime,
this.planModificationSource,
// 🚀 新增:沉浸模式相关状态
this.isImmersiveMode = false,
this.immersiveChapterId,
});
final novel_models.Novel novel;
final Map<String, dynamic> settings;
final String? activeActId;
final String? activeChapterId;
final String? activeSceneId;
final String? focusChapterId;
final bool isDirty;
final bool isSaving;
final bool isLoading;
final bool hasReachedEnd;
final bool hasReachedStart;
final DateTime? lastSaveTime;
final String? errorMessage;
final bool isStreamingGeneration;
final String? pendingSummary;
final List<int>? visibleRange;
final bool virtualListEnabled;
final Map<String, int> chapterGlobalIndices;
final Map<String, String> chapterToActMap;
/// AI生成状态
final AIGenerationStatus aiSummaryGenerationStatus;
/// AI生成场景状态
final AIGenerationStatus aiSceneGenerationStatus;
/// AI生成的摘要内容
final String? generatedSummary;
/// AI生成的场景内容
final String? generatedSceneContent;
/// AI生成过程中的错误消息
final String? aiGenerationError;
final bool lastUpdateSilent;
// 🚀 新增Plan视图相关状态
final bool isPlanViewMode; // 是否处于Plan视图模式
final bool planViewDirty; // Plan视图是否有未保存的修改
final DateTime? lastPlanModifiedTime; // Plan视图最后修改时间
final String? planModificationSource; // Plan修改的来源用于跟踪是否需要刷新Write视图
// 🚀 新增:沉浸模式相关状态
final bool isImmersiveMode; // 是否处于沉浸模式
final String? immersiveChapterId; // 沉浸模式下当前显示的章节ID
@override
List<Object?> get props => [
settings,
activeActId,
activeChapterId,
activeSceneId,
focusChapterId,
isDirty,
isSaving,
isLoading,
hasReachedEnd,
hasReachedStart,
lastSaveTime,
errorMessage,
aiSummaryGenerationStatus,
aiSceneGenerationStatus,
generatedSummary,
generatedSceneContent,
aiGenerationError,
isStreamingGeneration,
pendingSummary,
visibleRange,
virtualListEnabled,
chapterGlobalIndices,
chapterToActMap,
lastUpdateSilent,
isPlanViewMode,
planViewDirty,
lastPlanModifiedTime,
planModificationSource,
isImmersiveMode,
immersiveChapterId,
];
EditorLoaded copyWith({
novel_models.Novel? novel,
Map<String, dynamic>? settings,
String? activeActId,
String? activeChapterId,
String? activeSceneId,
String? focusChapterId,
bool? isDirty,
bool? isSaving,
bool? isLoading,
bool? hasReachedEnd,
bool? hasReachedStart,
DateTime? lastSaveTime,
String? errorMessage,
AIGenerationStatus? aiSummaryGenerationStatus,
AIGenerationStatus? aiSceneGenerationStatus,
String? generatedSummary,
String? generatedSceneContent,
String? aiGenerationError,
bool? isStreamingGeneration,
String? pendingSummary,
List<int>? visibleRange,
bool? virtualListEnabled,
Map<String, int>? chapterGlobalIndices,
Map<String, String>? chapterToActMap,
bool? lastUpdateSilent,
bool? isPlanViewMode,
bool? planViewDirty,
DateTime? lastPlanModifiedTime,
String? planModificationSource,
// 🚀 新增:沉浸模式参数
bool? isImmersiveMode,
String? immersiveChapterId,
}) {
return EditorLoaded(
novel: novel ?? this.novel,
settings: settings ?? this.settings,
activeActId: activeActId ?? this.activeActId,
activeChapterId: activeChapterId ?? this.activeChapterId,
activeSceneId: activeSceneId ?? this.activeSceneId,
focusChapterId: focusChapterId ?? this.focusChapterId,
isDirty: isDirty ?? this.isDirty,
isSaving: isSaving ?? this.isSaving,
isLoading: isLoading ?? this.isLoading,
hasReachedEnd: hasReachedEnd ?? this.hasReachedEnd,
hasReachedStart: hasReachedStart ?? this.hasReachedStart,
lastSaveTime: lastSaveTime ?? this.lastSaveTime,
errorMessage: errorMessage,
aiSummaryGenerationStatus: aiSummaryGenerationStatus ?? this.aiSummaryGenerationStatus,
aiSceneGenerationStatus: aiSceneGenerationStatus ?? this.aiSceneGenerationStatus,
generatedSummary: generatedSummary ?? this.generatedSummary,
generatedSceneContent: generatedSceneContent ?? this.generatedSceneContent,
aiGenerationError: aiGenerationError,
isStreamingGeneration: isStreamingGeneration ?? this.isStreamingGeneration,
pendingSummary: pendingSummary,
visibleRange: visibleRange ?? this.visibleRange,
virtualListEnabled: virtualListEnabled ?? this.virtualListEnabled,
chapterGlobalIndices: chapterGlobalIndices ?? this.chapterGlobalIndices,
chapterToActMap: chapterToActMap ?? this.chapterToActMap,
lastUpdateSilent: lastUpdateSilent ?? this.lastUpdateSilent,
isPlanViewMode: isPlanViewMode ?? this.isPlanViewMode,
planViewDirty: planViewDirty ?? this.planViewDirty,
lastPlanModifiedTime: lastPlanModifiedTime ?? this.lastPlanModifiedTime,
planModificationSource: planModificationSource ?? this.planModificationSource,
// 🚀 新增:沉浸模式状态赋值
isImmersiveMode: isImmersiveMode ?? this.isImmersiveMode,
immersiveChapterId: immersiveChapterId ?? this.immersiveChapterId,
);
}
}
class EditorSettingsOpen extends EditorState {
const EditorSettingsOpen({
required this.novel,
required this.settings,
this.activeActId,
this.activeChapterId,
this.activeSceneId,
this.isDirty = false,
});
final novel_models.Novel novel;
final Map<String, dynamic> settings;
final String? activeActId;
final String? activeChapterId;
final String? activeSceneId;
final bool isDirty;
@override
List<Object?> get props => [
novel,
settings,
activeActId,
activeChapterId,
activeSceneId,
isDirty,
];
EditorSettingsOpen copyWith({
novel_models.Novel? novel,
Map<String, dynamic>? settings,
String? activeActId,
String? activeChapterId,
String? activeSceneId,
bool? isDirty,
}) {
return EditorSettingsOpen(
novel: novel ?? this.novel,
settings: settings ?? this.settings,
activeActId: activeActId ?? this.activeActId,
activeChapterId: activeChapterId ?? this.activeChapterId,
activeSceneId: activeSceneId ?? this.activeSceneId,
isDirty: isDirty ?? this.isDirty,
);
}
}
class EditorError extends EditorState {
const EditorError({required this.message});
final String message;
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,186 @@
import 'dart:async';
import 'package:ainoval/models/novel_structure.dart';
import 'package:ainoval/models/scene_version.dart';
import 'package:ainoval/services/api_service/repositories/novel_repository.dart' as api;
import 'package:ainoval/ui/dialogs/scene_history_dialog.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
part 'editor_version_event.dart';
part 'editor_version_state.dart';
/// 编辑器版本控制Bloc
class EditorVersionBloc extends Bloc<EditorVersionEvent, EditorVersionState> {
EditorVersionBloc({
required api.NovelRepository novelRepository,
}) : _novelRepository = novelRepository,
super(EditorVersionInitial()) {
on<EditorVersionFetchHistory>(_onFetchHistory);
on<EditorVersionCompare>(_onCompareVersions);
on<EditorVersionRestore>(_onRestoreVersion);
on<EditorVersionSave>(_onSaveVersion);
}
final api.NovelRepository _novelRepository;
/// 获取场景历史版本
Future<void> _onFetchHistory(
EditorVersionFetchHistory event,
Emitter<EditorVersionState> emit,
) async {
if (event.novelId.isEmpty || event.chapterId.isEmpty || event.sceneId.isEmpty) {
emit(const EditorVersionError('无效的场景ID'));
return;
}
emit(EditorVersionLoading());
try {
final history = await _novelRepository.getSceneHistory(
event.novelId,
event.chapterId,
event.sceneId,
);
if (history.isEmpty) {
emit(EditorVersionHistoryEmpty());
} else {
emit(EditorVersionHistoryLoaded(history));
}
} catch (e) {
AppLogger.e('Blocs/editor_version_bloc', '获取历史版本失败', e);
emit(EditorVersionError('获取历史版本失败: $e'));
}
}
/// 比较版本差异
Future<void> _onCompareVersions(
EditorVersionCompare event,
Emitter<EditorVersionState> emit,
) async {
if (event.novelId.isEmpty || event.chapterId.isEmpty || event.sceneId.isEmpty) {
emit(const EditorVersionError('无效的场景ID'));
return;
}
emit(EditorVersionLoading());
try {
final diff = await _novelRepository.compareSceneVersions(
event.novelId,
event.chapterId,
event.sceneId,
event.versionIndex1,
event.versionIndex2,
);
emit(EditorVersionDiffLoaded(diff));
} catch (e) {
AppLogger.e('Blocs/editor_version_bloc', '比较版本差异失败', e);
emit(EditorVersionError('比较版本差异失败: $e'));
}
}
/// 恢复到历史版本
Future<void> _onRestoreVersion(
EditorVersionRestore event,
Emitter<EditorVersionState> emit,
) async {
if (event.novelId.isEmpty || event.chapterId.isEmpty || event.sceneId.isEmpty) {
emit(const EditorVersionError('无效的场景ID'));
return;
}
emit(EditorVersionLoading());
try {
final scene = await _novelRepository.restoreSceneVersion(
event.novelId,
event.chapterId,
event.sceneId,
event.historyIndex,
event.userId,
event.reason,
);
emit(EditorVersionRestored(scene));
} catch (e) {
AppLogger.e('Blocs/editor_version_bloc', '恢复版本失败', e);
emit(EditorVersionError('恢复版本失败: $e'));
}
}
/// 保存新版本
Future<void> _onSaveVersion(
EditorVersionSave event,
Emitter<EditorVersionState> emit,
) async {
if (event.novelId.isEmpty || event.chapterId.isEmpty || event.sceneId.isEmpty) {
emit(const EditorVersionError('无效的场景ID'));
return;
}
emit(EditorVersionLoading());
try {
final scene = await _novelRepository.updateSceneContentWithHistory(
event.novelId,
event.chapterId,
event.sceneId,
event.content,
event.userId,
event.reason,
);
emit(EditorVersionSaved(scene));
} catch (e) {
AppLogger.e('Blocs/editor_version_bloc', '保存版本失败', e);
emit(EditorVersionError('保存版本失败: $e'));
}
}
/// 保存新版本并添加到历史记录
Future<bool> saveVersionWithReason(
String novelId,
String chapterId,
String sceneId,
String content,
String userId,
String reason,
) async {
try {
add(EditorVersionSave(
novelId: novelId,
chapterId: chapterId,
sceneId: sceneId,
content: content,
userId: userId,
reason: reason,
));
return true;
} catch (e) {
AppLogger.e('Blocs/editor_version_bloc', '保存版本失败', e);
return false;
}
}
/// 打开历史版本对话框
Future<Scene?> openHistoryDialog(
BuildContext context,
String novelId,
String chapterId,
String sceneId,
) async {
return await showDialog<Scene>(
context: context,
builder: (context) => SceneHistoryDialog(
novelId: novelId,
chapterId: chapterId,
sceneId: sceneId,
),
);
}
}

View File

@@ -0,0 +1,109 @@
part of 'editor_version_bloc.dart';
/// 编辑器版本控制事件
abstract class EditorVersionEvent extends Equatable {
const EditorVersionEvent();
@override
List<Object?> get props => [];
}
/// 获取版本历史记录事件
class EditorVersionFetchHistory extends EditorVersionEvent {
const EditorVersionFetchHistory({
required this.novelId,
required this.chapterId,
required this.sceneId,
});
final String novelId;
final String chapterId;
final String sceneId;
@override
List<Object?> get props => [novelId, chapterId, sceneId];
}
/// 比较版本差异事件
class EditorVersionCompare extends EditorVersionEvent {
const EditorVersionCompare({
required this.novelId,
required this.chapterId,
required this.sceneId,
required this.versionIndex1,
required this.versionIndex2,
});
final String novelId;
final String chapterId;
final String sceneId;
final int versionIndex1;
final int versionIndex2;
@override
List<Object?> get props => [
novelId,
chapterId,
sceneId,
versionIndex1,
versionIndex2,
];
}
/// 恢复版本事件
class EditorVersionRestore extends EditorVersionEvent {
const EditorVersionRestore({
required this.novelId,
required this.chapterId,
required this.sceneId,
required this.historyIndex,
required this.userId,
required this.reason,
});
final String novelId;
final String chapterId;
final String sceneId;
final int historyIndex;
final String userId;
final String reason;
@override
List<Object?> get props => [
novelId,
chapterId,
sceneId,
historyIndex,
userId,
reason,
];
}
/// 保存版本事件
class EditorVersionSave extends EditorVersionEvent {
const EditorVersionSave({
required this.novelId,
required this.chapterId,
required this.sceneId,
required this.content,
required this.userId,
required this.reason,
});
final String novelId;
final String chapterId;
final String sceneId;
final String content;
final String userId;
final String reason;
@override
List<Object?> get props => [
novelId,
chapterId,
sceneId,
content,
userId,
reason,
];
}

View File

@@ -0,0 +1,68 @@
part of 'editor_version_bloc.dart';
/// 编辑器版本控制状态
abstract class EditorVersionState extends Equatable {
const EditorVersionState();
@override
List<Object?> get props => [];
}
/// 初始状态
class EditorVersionInitial extends EditorVersionState {}
/// 加载中状态
class EditorVersionLoading extends EditorVersionState {}
/// 版本历史记录加载完成状态
class EditorVersionHistoryLoaded extends EditorVersionState {
const EditorVersionHistoryLoaded(this.history);
final List<SceneHistoryEntry> history;
@override
List<Object?> get props => [history];
}
/// 版本历史为空状态
class EditorVersionHistoryEmpty extends EditorVersionState {}
/// 版本差异加载完成状态
class EditorVersionDiffLoaded extends EditorVersionState {
const EditorVersionDiffLoaded(this.diff);
final SceneVersionDiff diff;
@override
List<Object?> get props => [diff];
}
/// 版本恢复完成状态
class EditorVersionRestored extends EditorVersionState {
const EditorVersionRestored(this.scene);
final Scene scene;
@override
List<Object?> get props => [scene];
}
/// 版本保存完成状态
class EditorVersionSaved extends EditorVersionState {
const EditorVersionSaved(this.scene);
final Scene scene;
@override
List<Object?> get props => [scene];
}
/// 错误状态
class EditorVersionError extends EditorVersionState {
const EditorVersionError(this.message);
final String message;
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,656 @@
import 'dart:async';
import 'package:ainoval/blocs/next_outline/next_outline_event.dart';
import 'package:ainoval/blocs/next_outline/next_outline_state.dart';
import 'package:ainoval/models/next_outline/next_outline_dto.dart';
import 'package:ainoval/models/next_outline/outline_generation_chunk.dart';
import 'package:ainoval/models/novel_structure.dart' as novel_models;
import 'package:ainoval/services/api_service/base/api_exception.dart';
import 'package:ainoval/services/api_service/repositories/editor_repository.dart';
import 'package:ainoval/services/api_service/repositories/next_outline_repository.dart';
import 'package:ainoval/services/api_service/repositories/user_ai_model_config_repository.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:ainoval/utils/event_bus.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ainoval/config/app_config.dart';
/// 剧情推演BLoC
class NextOutlineBloc extends Bloc<NextOutlineEvent, NextOutlineState> {
final NextOutlineRepository _nextOutlineRepository;
final EditorRepository _editorRepository;
final UserAIModelConfigRepository _userAIModelConfigRepository;
// 存储活跃的流订阅
final Map<String, StreamSubscription> _activeSubscriptions = {};
final String _tag = 'NextOutlineBloc';
NextOutlineBloc({
required NextOutlineRepository nextOutlineRepository,
required EditorRepository editorRepository,
required UserAIModelConfigRepository userAIModelConfigRepository,
}) : _nextOutlineRepository = nextOutlineRepository,
_editorRepository = editorRepository,
_userAIModelConfigRepository = userAIModelConfigRepository,
super(NextOutlineState.initial(novelId: '')) {
on<NextOutlineInitialized>(_onInitialized);
on<LoadChaptersRequested>(_onLoadChaptersRequested);
on<LoadAIModelConfigsRequested>(_onLoadAIModelConfigsRequested);
on<UpdateChapterRangeRequested>(_onUpdateChapterRangeRequested);
on<GenerateNextOutlinesRequested>(_onGenerateNextOutlinesRequested);
on<RegenerateAllOutlinesRequested>(_onRegenerateAllOutlinesRequested);
on<RegenerateSingleOutlineRequested>(_onRegenerateSingleOutlineRequested);
on<OutlineSelected>(_onOutlineSelected);
on<SaveSelectedOutlineRequested>(_onSaveSelectedOutlineRequested);
on<OutlineGenerationChunkReceived>(_onOutlineGenerationChunkReceived);
on<GenerationErrorOccurred>(_onGenerationErrorOccurred);
}
/// 初始化
Future<void> _onInitialized(
NextOutlineInitialized event,
Emitter<NextOutlineState> emit,
) async {
emit(NextOutlineState.initial(novelId: event.novelId));
// 加载章节和AI模型配置
add(LoadChaptersRequested(novelId: event.novelId));
add(const LoadAIModelConfigsRequested());
}
/// 加载章节列表
Future<void> _onLoadChaptersRequested(
LoadChaptersRequested event,
Emitter<NextOutlineState> emit,
) async {
try {
emit(state.copyWith(
generationStatus: GenerationStatus.loadingChapters,
clearError: true,
));
// 获取小说数据,从中提取章节列表
final novel = await _editorRepository.getNovel(event.novelId);
List<novel_models.Chapter> chapters = [];
String? startChapterId;
String? endChapterId;
if (novel != null) {
// 提取所有章节
for (final act in novel.acts) {
chapters.addAll(act.chapters);
}
}
// 默认范围:从第一章到最后一章(用于剧情推演的上下文)
if (chapters.isNotEmpty) {
startChapterId = chapters.first.id;
endChapterId = chapters.last.id;
AppLogger.i(_tag, '设置默认章节范围: 从第一章(${chapters.first.title}) 到最后一章(${chapters.last.title})');
}
emit(state.copyWith(
chapters: chapters,
startChapterId: startChapterId,
endChapterId: endChapterId,
generationStatus: GenerationStatus.idle,
));
} catch (e) {
AppLogger.e(_tag, '加载章节失败', e);
emit(state.copyWith(
generationStatus: GenerationStatus.error,
errorMessage: '加载章节失败: $e',
));
}
}
/// 加载AI模型配置
Future<void> _onLoadAIModelConfigsRequested(
LoadAIModelConfigsRequested event,
Emitter<NextOutlineState> emit,
) async {
try {
emit(state.copyWith(
generationStatus: GenerationStatus.loadingModels,
));
// 从AppConfig获取当前用户ID而不是使用硬编码的"current"
final String userId = AppConfig.userId ?? '';
final configs = await _userAIModelConfigRepository.listConfigurations(userId: userId);
emit(state.copyWith(
aiModelConfigs: configs,
generationStatus: GenerationStatus.idle,
clearError: true,
));
} catch (e) {
AppLogger.e(_tag, '加载AI模型配置失败', e);
// 不进入错误状态,而是使用空配置列表继续
emit(state.copyWith(
aiModelConfigs: [], // 使用空配置列表
generationStatus: GenerationStatus.idle, // 改为idle状态而不是error
clearError: true, // 清除错误
));
AppLogger.w(_tag, '使用空AI模型配置列表继续生成时将使用后端默认配置');
}
}
/// 更新上下文章节范围
void _onUpdateChapterRangeRequested(
UpdateChapterRangeRequested event,
Emitter<NextOutlineState> emit,
) {
// 验证章节顺序
String? errorMessage;
if (event.startChapterId != null && event.endChapterId != null && state.chapters.isNotEmpty) {
// 查找章节索引
int? startIndex;
int? endIndex;
for (int i = 0; i < state.chapters.length; i++) {
if (state.chapters[i].id == event.startChapterId) {
startIndex = i;
}
if (state.chapters[i].id == event.endChapterId) {
endIndex = i;
}
// 如果两个索引都找到了,可以提前结束循环
if (startIndex != null && endIndex != null) {
break;
}
}
// 检查有效性
if (startIndex != null && endIndex != null && startIndex > endIndex) {
errorMessage = '起始章节不能晚于结束章节';
AppLogger.w(_tag, errorMessage);
}
}
emit(state.copyWith(
startChapterId: event.startChapterId,
endChapterId: event.endChapterId,
errorMessage: errorMessage,
clearError: errorMessage == null,
));
}
/// 生成剧情大纲
Future<void> _onGenerateNextOutlinesRequested(
GenerateNextOutlinesRequested event,
Emitter<NextOutlineState> emit,
) async {
try {
// 取消所有活跃的流订阅
_cancelAllSubscriptions();
// 处理章节范围如果没有提供startChapterId使用第一章
String? finalStartChapterId = event.request.startChapterId;
String? finalEndChapterId = event.request.endChapterId;
if (finalStartChapterId == null && state.chapters.isNotEmpty) {
finalStartChapterId = state.chapters.first.id;
AppLogger.i(_tag, '未提供startChapterId使用第一章: ${state.chapters.first.title}');
}
if (finalEndChapterId == null && state.chapters.isNotEmpty) {
finalEndChapterId = state.chapters.last.id;
AppLogger.i(_tag, '未提供endChapterId使用最后一章: ${state.chapters.last.title}');
}
// 处理默认AI配置如果没有提供selectedConfigIds使用前3个可用配置
List<String>? finalConfigIds = event.request.selectedConfigIds;
if (finalConfigIds == null || finalConfigIds.isEmpty) {
if (state.aiModelConfigs.isNotEmpty) {
final configCount = state.aiModelConfigs.length;
final useCount = configCount >= event.request.numOptions ? event.request.numOptions : configCount;
finalConfigIds = state.aiModelConfigs
.take(useCount)
.map((config) => config.id)
.toList();
AppLogger.i(_tag, '使用默认AI配置: ${finalConfigIds.join(", ")}');
} else {
// 如果没有可用的AI配置使用null让后端选择默认配置
finalConfigIds = null;
AppLogger.w(_tag, '没有可用的AI配置使用null让后端选择默认配置');
}
}
// 创建修正后的请求
final correctedRequest = GenerateNextOutlinesRequest(
startChapterId: finalStartChapterId,
endChapterId: finalEndChapterId,
numOptions: event.request.numOptions,
authorGuidance: event.request.authorGuidance,
selectedConfigIds: finalConfigIds,
regenerateHint: event.request.regenerateHint,
);
emit(state.copyWith(
generationStatus: GenerationStatus.generatingInitial,
outlineOptions: [],
clearSelectedOption: true,
clearError: true,
numOptions: correctedRequest.numOptions,
authorGuidance: correctedRequest.authorGuidance,
));
AppLogger.i(_tag, '开始生成剧情大纲: startChapter=${correctedRequest.startChapterId}, endChapter=${correctedRequest.endChapterId}, numOptions=${correctedRequest.numOptions}, configs=${finalConfigIds?.join(", ")}');
// 订阅流式响应
final stream = _nextOutlineRepository.generateNextOutlinesStream(
state.novelId,
correctedRequest,
);
final subscription = stream.listen(
(chunk) {
// 处理接收到的块
add(OutlineGenerationChunkReceived(
optionId: chunk.optionId,
optionTitle: chunk.optionTitle,
textChunk: chunk.textChunk,
isFinalChunk: chunk.isFinalChunk,
error: chunk.error,
));
},
onError: (error) {
AppLogger.e(_tag, '生成剧情大纲流错误', error);
String errorMessage = error.toString();
if (error is ApiException) {
errorMessage = error.message;
}
// 不再尝试关联特定选项,直接触发全局错误处理
add(GenerationErrorOccurred(error: errorMessage));
},
onDone: () {
AppLogger.i(_tag, '生成剧情大纲流完成');
// 检查是否所有选项都已完成
_checkAllOptionsComplete(emit);
},
);
// 存储订阅
_activeSubscriptions['generate'] = subscription;
} catch (e) {
AppLogger.e(_tag, '生成剧情大纲失败', e);
emit(state.copyWith(
generationStatus: GenerationStatus.error,
errorMessage: '生成剧情大纲失败: $e',
));
}
}
/// 重新生成全部剧情大纲
Future<void> _onRegenerateAllOutlinesRequested(
RegenerateAllOutlinesRequested event,
Emitter<NextOutlineState> emit,
) async {
try {
// 构建重新生成请求
final request = GenerateNextOutlinesRequest(
startChapterId: state.startChapterId,
endChapterId: state.endChapterId,
numOptions: state.numOptions,
authorGuidance: state.authorGuidance,
regenerateHint: event.regenerateHint,
selectedConfigIds: state.aiModelConfigs.isNotEmpty
? state.aiModelConfigs
.take(state.numOptions)
.map((config) => config.id)
.toList()
: null,
);
// 调用生成事件
add(GenerateNextOutlinesRequested(request: request));
} catch (e) {
AppLogger.e(_tag, '重新生成所有剧情大纲失败', e);
emit(state.copyWith(
generationStatus: GenerationStatus.error,
errorMessage: '重新生成所有剧情大纲失败: $e',
));
}
}
/// 重新生成单个剧情大纲
Future<void> _onRegenerateSingleOutlineRequested(
RegenerateSingleOutlineRequested event,
Emitter<NextOutlineState> emit,
) async {
try {
// 找到要重新生成的选项
final optionIndex = state.outlineOptions
.indexWhere((option) => option.optionId == event.request.optionId);
if (optionIndex == -1) {
throw Exception('未找到指定的剧情选项');
}
// 取消该选项的现有订阅
final subKey = 'regenerate_${event.request.optionId}';
if (_activeSubscriptions.containsKey(subKey)) {
_activeSubscriptions[subKey]?.cancel();
_activeSubscriptions.remove(subKey);
}
// 更新选项状态为生成中
final updatedOptions = List<OutlineOptionState>.from(state.outlineOptions);
updatedOptions[optionIndex] = updatedOptions[optionIndex].copyWith(
isGenerating: true,
isComplete: false,
);
emit(state.copyWith(
outlineOptions: updatedOptions,
generationStatus: GenerationStatus.generatingSingle,
clearError: true,
));
// 订阅流式响应
final stream = _nextOutlineRepository.regenerateOutlineOption(
state.novelId,
event.request,
);
final subscription = stream.listen(
(chunk) {
// 处理接收到的块
add(OutlineGenerationChunkReceived(
optionId: chunk.optionId,
optionTitle: chunk.optionTitle,
textChunk: chunk.textChunk,
isFinalChunk: chunk.isFinalChunk,
error: chunk.error,
));
},
onError: (error) {
AppLogger.e(_tag, '重新生成单个剧情大纲流错误', error);
String errorMessage = error.toString();
if (error is ApiException) {
errorMessage = error.message;
}
// 更新对应选项的错误状态,而不是全局错误
final errorOptionIndex = state.outlineOptions
.indexWhere((option) => option.optionId == event.request.optionId);
if (errorOptionIndex != -1) {
final updatedErrorOptions = List<OutlineOptionState>.from(state.outlineOptions);
updatedErrorOptions[errorOptionIndex] = updatedErrorOptions[errorOptionIndex].copyWith(
isGenerating: false,
isComplete: true,
errorMessage: errorMessage,
);
emit(state.copyWith(
outlineOptions: updatedErrorOptions,
));
_checkAllOptionsComplete(emit);
} else {
// 如果找不到选项,回退到全局错误
add(GenerationErrorOccurred(error: errorMessage));
}
},
onDone: () {
AppLogger.i(_tag, '重新生成单个剧情大纲流完成');
// 检查是否所有选项都已完成
_checkAllOptionsComplete(emit);
},
);
// 存储订阅
_activeSubscriptions[subKey] = subscription;
} catch (e) {
AppLogger.e(_tag, '重新生成单个剧情大纲失败', e);
emit(state.copyWith(
generationStatus: GenerationStatus.error,
errorMessage: '重新生成单个剧情大纲失败: $e',
));
}
}
/// 选择剧情大纲
void _onOutlineSelected(
OutlineSelected event,
Emitter<NextOutlineState> emit,
) {
// 获取选择的选项索引
final optionIndex = state.outlineOptions.indexWhere((option) => option.optionId == event.optionId);
// 如果找到选项且outputGeneration存在
if (optionIndex != -1 && state.outputGeneration != null) {
// 创建新的outputGeneration更新selectedOutlineIndex
final updatedOutputGeneration = NextOutlineOutput(
outlineList: state.outputGeneration!.outlineList,
generationTimeMs: state.outputGeneration!.generationTimeMs,
selectedOutlineIndex: optionIndex,
);
emit(state.copyWith(
selectedOptionId: event.optionId,
outputGeneration: updatedOutputGeneration,
clearError: true,
));
} else {
// 仅更新选项ID
emit(state.copyWith(
selectedOptionId: event.optionId,
clearError: true,
));
}
}
/// 保存选中的剧情大纲
Future<void> _onSaveSelectedOutlineRequested(
SaveSelectedOutlineRequested event,
Emitter<NextOutlineState> emit,
) async {
try {
// 设置状态为保存中
emit(state.copyWith(
generationStatus: GenerationStatus.saving,
clearError: true,
));
// 检查是否有选中的大纲索引
int? outlineIndex = event.selectedOutlineIndex;
// 如果没有传入索引但有选中的选项ID则尝试查找对应的索引
if (outlineIndex == null && state.selectedOptionId != null) {
AppLogger.i(_tag, '尝试使用selectedOptionId查找大纲索引');
final selectedOptionIndex = state.outlineOptions.indexWhere(
(option) => option.optionId == state.selectedOptionId
);
if (selectedOptionIndex != -1) {
outlineIndex = selectedOptionIndex;
AppLogger.i(_tag, '已找到对应索引: $outlineIndex');
}
}
// 检查输出生成结果和索引是否有效
if (outlineIndex == null || outlineIndex < 0 ||
state.outputGeneration == null ||
outlineIndex >= state.outputGeneration!.outlineList.length) {
final errorMsg = '未选择有效的大纲或大纲不存在: index=$outlineIndex, outputGeneration=${state.outputGeneration != null}, selectedOptionId=${state.selectedOptionId}';
AppLogger.e(_tag, errorMsg);
emit(state.copyWith(
generationStatus: GenerationStatus.error,
errorMessage: errorMsg,
));
return;
}
var selectedOutline = state.outputGeneration!.outlineList[outlineIndex];
AppLogger.i(_tag, '正在保存大纲: ${selectedOutline.title}');
// 调用保存API
final response = await _nextOutlineRepository.saveNextOutline(
state.novelId,
event.request,
);
// 保存成功
AppLogger.i(_tag, '剧情大纲保存成功');
// 发送小说结构更新事件
EventBus.instance.fire(NovelStructureUpdatedEvent(
novelId: state.novelId,
updateType: 'outline_saved',
data: {
'outlineId': event.request.outlineId,
'insertType': event.request.insertType,
'newChapterId': response.newChapterId,
'newSceneId': response.newSceneId,
'targetChapterId': response.targetChapterId,
'outline': selectedOutline.toJson(),
'apiResult': response.toJson(),
},
));
// 保持状态不变,只更新生成状态为空闲
emit(state.copyWith(
generationStatus: GenerationStatus.idle,
// 不更改其他状态,保留当前大纲和选项
));
} catch (e) {
AppLogger.e(_tag, '保存剧情大纲失败', e);
emit(state.copyWith(
generationStatus: GenerationStatus.error,
errorMessage: '保存剧情大纲失败: $e',
));
}
}
/// 处理生成块接收事件
void _onOutlineGenerationChunkReceived(
OutlineGenerationChunkReceived event,
Emitter<NextOutlineState> emit,
) {
try {
final List<OutlineOptionState> currentOptions = List.from(state.outlineOptions);
int optionIndex = currentOptions.indexWhere((option) => option.optionId == event.optionId);
OutlineOptionState updatedOption;
if (optionIndex == -1) {
// ---- 新增:动态创建新的选项状态 ----
AppLogger.i(_tag, '首次接收到选项 ${event.optionId} 的数据块,创建新的状态');
updatedOption = OutlineOptionState(
optionId: event.optionId,
title: event.optionTitle,
content: event.textChunk,
isGenerating: !event.isFinalChunk,
isComplete: event.isFinalChunk,
errorMessage: event.error, // 处理可能直接在chunk中传来的错误
);
currentOptions.add(updatedOption);
// -------------------------------
} else {
// ---- 更新现有选项状态 ----
final existingOption = currentOptions[optionIndex];
updatedOption = existingOption.copyWith(
// 追加内容
content: existingOption.content + event.textChunk,
// 更新标题(如果新的标题非空且不同)
title: (event.optionTitle != null && event.optionTitle!.isNotEmpty && event.optionTitle != existingOption.title)
? event.optionTitle
: existingOption.title,
// 更新状态
isGenerating: !event.isFinalChunk,
isComplete: event.isFinalChunk,
// 更新错误信息(如果新的错误信息非空)
errorMessage: event.error ?? existingOption.errorMessage,
);
currentOptions[optionIndex] = updatedOption;
// ------------------------
}
emit(state.copyWith(outlineOptions: currentOptions));
// 检查是否所有选项都已完成 (可以在这里检查,或者依赖 onDone)
if (currentOptions.every((o) => o.isComplete)) {
_checkAllOptionsComplete(emit);
}
} catch (e, stackTrace) {
AppLogger.e(_tag, '处理生成块失败 for ${event.optionId}', e, stackTrace);
// 考虑是否要将此错误设置到对应的option上或触发全局错误
// 为了避免影响其他流,暂时只记录日志
}
}
/// 处理生成错误事件
void _onGenerationErrorOccurred(
GenerationErrorOccurred event,
Emitter<NextOutlineState> emit,
) {
AppLogger.e(_tag, '全局生成错误: ${event.error}');
// 停止所有仍在进行的生成,并标记错误
final updatedOptions = state.outlineOptions.map((option) {
if (option.isGenerating) { // 只处理还在生成中的选项
return option.copyWith(
isGenerating: false,
isComplete: true, // 标记为完成(即使是失败)
errorMessage: event.error,
);
}
return option; // 其他选项保持不变
}).toList();
emit(state.copyWith(
generationStatus: GenerationStatus.error, // 设置全局状态为错误
errorMessage: event.error,
outlineOptions: updatedOptions, // 更新选项列表
));
}
/// 检查所有选项是否已完成生成
void _checkAllOptionsComplete(Emitter<NextOutlineState> emit) {
if (state.outlineOptions.every((option) => option.isComplete)) {
// 所有选项都已完成生成
// 将outlineOptions转换为NextOutlineOutput
final outlineList = state.outlineOptions.map((option) => NextOutlineDTO(
id: option.optionId,
title: option.title ?? 'Untitled Outline',
content: option.content,
configId: option.configId,
)).toList();
final outputGeneration = NextOutlineOutput(
outlineList: outlineList,
generationTimeMs: DateTime.now().millisecondsSinceEpoch,
selectedOutlineIndex: null, // 初始时没有选中的大纲
);
// 更新状态设置status为success
emit(state.copyWith(
generationStatus: GenerationStatus.idle,
status: NextOutlineStatus.success,
outputGeneration: outputGeneration,
));
}
}
/// 取消所有活跃的流订阅
void _cancelAllSubscriptions() {
_activeSubscriptions.forEach((key, subscription) {
subscription.cancel();
});
_activeSubscriptions.clear();
}
@override
Future<void> close() {
_cancelAllSubscriptions();
return super.close();
}
}

View File

@@ -0,0 +1,133 @@
import 'package:ainoval/models/next_outline/next_outline_dto.dart';
import 'package:equatable/equatable.dart';
/// 剧情推演事件
abstract class NextOutlineEvent extends Equatable {
const NextOutlineEvent();
@override
List<Object?> get props => [];
}
/// 初始化事件
class NextOutlineInitialized extends NextOutlineEvent {
final String novelId;
const NextOutlineInitialized({required this.novelId});
@override
List<Object?> get props => [novelId];
}
/// 加载章节列表事件
class LoadChaptersRequested extends NextOutlineEvent {
final String novelId;
const LoadChaptersRequested({required this.novelId});
@override
List<Object?> get props => [novelId];
}
/// 加载AI模型配置事件
class LoadAIModelConfigsRequested extends NextOutlineEvent {
const LoadAIModelConfigsRequested();
}
/// 更新上下文章节范围事件
class UpdateChapterRangeRequested extends NextOutlineEvent {
final String? startChapterId;
final String? endChapterId;
const UpdateChapterRangeRequested({
this.startChapterId,
this.endChapterId,
});
@override
List<Object?> get props => [startChapterId, endChapterId];
}
/// 生成剧情大纲事件
class GenerateNextOutlinesRequested extends NextOutlineEvent {
final GenerateNextOutlinesRequest request;
const GenerateNextOutlinesRequested({required this.request});
@override
List<Object?> get props => [request];
}
/// 重新生成全部剧情大纲事件
class RegenerateAllOutlinesRequested extends NextOutlineEvent {
final String? regenerateHint;
const RegenerateAllOutlinesRequested({this.regenerateHint});
@override
List<Object?> get props => [regenerateHint];
}
/// 重新生成单个剧情大纲事件
class RegenerateSingleOutlineRequested extends NextOutlineEvent {
final RegenerateOptionRequest request;
const RegenerateSingleOutlineRequested({required this.request});
@override
List<Object?> get props => [request];
}
/// 选择剧情大纲事件
class OutlineSelected extends NextOutlineEvent {
final String optionId;
const OutlineSelected({required this.optionId});
@override
List<Object?> get props => [optionId];
}
/// 保存选中的剧情大纲事件
class SaveSelectedOutlineRequested extends NextOutlineEvent {
final SaveNextOutlineRequest request;
final int? selectedOutlineIndex;
const SaveSelectedOutlineRequested({
required this.request,
this.selectedOutlineIndex,
});
@override
List<Object?> get props => [request, selectedOutlineIndex];
}
/// 接收到大纲生成块事件
class OutlineGenerationChunkReceived extends NextOutlineEvent {
final String optionId;
final String? optionTitle;
final String textChunk;
final bool isFinalChunk;
final String? error;
const OutlineGenerationChunkReceived({
required this.optionId,
this.optionTitle,
required this.textChunk,
required this.isFinalChunk,
this.error,
});
@override
List<Object?> get props => [optionId, optionTitle, textChunk, isFinalChunk, error];
}
/// 生成错误事件
class GenerationErrorOccurred extends NextOutlineEvent {
final String error;
const GenerationErrorOccurred({required this.error});
@override
List<Object?> get props => [error];
}

View File

@@ -0,0 +1,240 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import '../../models/novel_structure.dart';
import '../../models/user_ai_model_config_model.dart';
import '../../models/next_outline/next_outline_dto.dart';
/// 大纲状态枚举
enum NextOutlineStatus {
initial,
loading,
success,
failure,
}
/// 剧情推演状态
class NextOutlineState extends Equatable {
/// 小说ID
final String novelId;
/// 章节列表
final List<Chapter> chapters;
/// AI模型配置列表
final List<UserAIModelConfigModel> aiModelConfigs;
/// 当前选中的上下文开始章节ID
final String? startChapterId;
/// 当前选中的上下文结束章节ID
final String? endChapterId;
/// 生成状态
final GenerationStatus generationStatus;
/// 剧情选项列表
final List<OutlineOptionState> outlineOptions;
/// 当前选中的剧情选项ID
final String? selectedOptionId;
/// 错误信息
final String? errorMessage;
/// 生成选项数量
final int numOptions;
/// 作者引导
final String? authorGuidance;
/// 大纲状态
final NextOutlineStatus status;
/// 是否正在保存
final bool isSaving;
/// 输出的大纲生成结果
final NextOutlineOutput? outputGeneration;
const NextOutlineState({
required this.novelId,
this.chapters = const [],
this.aiModelConfigs = const [],
this.startChapterId,
this.endChapterId,
this.generationStatus = GenerationStatus.initial,
this.outlineOptions = const [],
this.selectedOptionId,
this.errorMessage,
this.numOptions = 3,
this.authorGuidance,
this.status = NextOutlineStatus.initial,
this.isSaving = false,
this.outputGeneration,
});
/// 初始状态
factory NextOutlineState.initial({required String novelId}) {
return NextOutlineState(
novelId: novelId,
);
}
/// 复制并修改状态
NextOutlineState copyWith({
String? novelId,
List<Chapter>? chapters,
List<UserAIModelConfigModel>? aiModelConfigs,
String? startChapterId,
String? endChapterId,
GenerationStatus? generationStatus,
List<OutlineOptionState>? outlineOptions,
String? selectedOptionId,
String? errorMessage,
int? numOptions,
String? authorGuidance,
NextOutlineStatus? status,
bool? isSaving,
NextOutlineOutput? outputGeneration,
bool clearError = false,
bool clearSelectedOption = false,
}) {
return NextOutlineState(
novelId: novelId ?? this.novelId,
chapters: chapters ?? this.chapters,
aiModelConfigs: aiModelConfigs ?? this.aiModelConfigs,
startChapterId: startChapterId ?? this.startChapterId,
endChapterId: endChapterId ?? this.endChapterId,
generationStatus: generationStatus ?? this.generationStatus,
outlineOptions: outlineOptions ?? this.outlineOptions,
selectedOptionId: clearSelectedOption ? null : (selectedOptionId ?? this.selectedOptionId),
errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
numOptions: numOptions ?? this.numOptions,
authorGuidance: authorGuidance ?? this.authorGuidance,
status: status ?? this.status,
isSaving: isSaving ?? this.isSaving,
outputGeneration: outputGeneration ?? this.outputGeneration,
);
}
@override
List<Object?> get props => [
novelId,
chapters,
aiModelConfigs,
startChapterId,
endChapterId,
generationStatus,
outlineOptions,
selectedOptionId,
errorMessage,
numOptions,
authorGuidance,
status,
isSaving,
outputGeneration,
];
}
/// 生成状态枚举
enum GenerationStatus {
initial,
loadingChapters,
loadingModels,
generatingInitial,
generatingSingle,
idle,
error,
saving,
}
/// 剧情选项状态
class OutlineOptionState extends Equatable {
/// 选项ID
final String optionId;
/// 标题
final String? title;
/// 内容
final String content;
/// 是否正在生成
final bool isGenerating;
/// 是否生成完成
final bool isComplete;
/// 使用的模型配置ID
final String? configId;
/// 内容流控制器
final ValueNotifier<String> contentStreamController;
/// 错误信息
final String? errorMessage;
OutlineOptionState({
required this.optionId,
this.title = '',
this.content = '',
this.isGenerating = false,
this.isComplete = false,
this.configId,
this.errorMessage,
}) : contentStreamController = ValueNotifier<String>(content);
/// 复制并修改状态
OutlineOptionState copyWith({
String? optionId,
String? title,
String? content,
bool? isGenerating,
bool? isComplete,
String? configId,
String? errorMessage,
}) {
final newContent = content ?? this.content;
final result = OutlineOptionState(
optionId: optionId ?? this.optionId,
title: title ?? this.title,
content: newContent,
isGenerating: isGenerating ?? this.isGenerating,
isComplete: isComplete ?? this.isComplete,
configId: configId ?? this.configId,
errorMessage: errorMessage ?? this.errorMessage,
);
// 更新内容流
if (content != null) {
result.contentStreamController.value = newContent;
}
return result;
}
/// 添加内容
OutlineOptionState addContent(String newContent) {
final updatedContent = content + newContent;
final result = copyWith(
content: updatedContent,
);
// 更新内容流
result.contentStreamController.value = updatedContent;
return result;
}
@override
List<Object?> get props => [
optionId,
title,
content,
isGenerating,
isComplete,
configId,
errorMessage,
];
}

View File

@@ -0,0 +1,316 @@
import 'dart:async';
import 'package:ainoval/models/import_status.dart';
import 'package:ainoval/models/user_ai_model_config_model.dart';
import 'package:ainoval/services/api_service/repositories/novel_repository.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:file_picker/file_picker.dart';
part 'novel_import_event.dart';
part 'novel_import_state.dart';
/// 小说导入Bloc - 支持新的三步导入流程
class NovelImportBloc extends Bloc<NovelImportEvent, NovelImportState> {
/// 创建小说导入Bloc
NovelImportBloc({required this.novelRepository})
: super(NovelImportInitial()) {
// 第一步:上传文件获取预览
on<UploadFileForPreview>(_onUploadFileForPreview);
// 第二步:获取导入预览
on<GetImportPreview>(_onGetImportPreview);
// 第三步:确认并开始导入
on<ConfirmAndStartImport>(_onConfirmAndStartImport);
// 导入状态更新
on<ImportStatusUpdate>(_onImportStatusUpdate);
// 重置状态
on<ResetImportState>(_onResetImportState);
// 清理预览会话
on<CleanupPreviewSession>(_onCleanupPreviewSession);
// 传统导入(向后兼容)
on<ImportNovelFile>(_onImportNovelFile);
}
/// 小说仓库
final NovelRepository novelRepository;
/// 导入状态订阅
StreamSubscription<ImportStatus>? _importStatusSubscription;
/// 处理上传文件获取预览事件
Future<void> _onUploadFileForPreview(
UploadFileForPreview event, Emitter<NovelImportState> emit) async {
emit(NovelImportUploading(message: '正在上传文件...'));
try {
// 选择文件
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['txt'],
withData: true,
);
if (result == null || result.files.isEmpty) {
emit(NovelImportInitial());
return;
}
final file = result.files.first;
final fileBytes = file.bytes;
final fileName = file.name;
if (fileBytes == null) {
emit(NovelImportFailure(message: '无法读取文件数据'));
return;
}
emit(NovelImportUploading(message: '正在上传文件到服务器...'));
// 上传文件并获取预览会话ID
final previewSessionId = await novelRepository.uploadFileForPreview(fileBytes, fileName);
emit(NovelImportFileUploaded(
previewSessionId: previewSessionId,
fileName: fileName,
fileSize: fileBytes.length,
));
} catch (e) {
AppLogger.e('NovelImportBloc', '上传文件失败', e);
emit(NovelImportFailure(message: '上传文件失败: ${e.toString()}'));
}
}
/// 处理获取导入预览事件
Future<void> _onGetImportPreview(
GetImportPreview event, Emitter<NovelImportState> emit) async {
emit(NovelImportLoadingPreview(message: '正在解析文件...'));
try {
// 获取导入预览
final responseData = await novelRepository.getImportPreview(
fileSessionId: event.previewSessionId,
customTitle: event.customTitle,
chapterLimit: event.chapterLimit,
enableSmartContext: event.enableSmartContext,
enableAISummary: event.enableAISummary,
aiConfigId: event.aiConfigId,
previewChapterCount: event.previewChapterCount,
);
// 转换为ImportPreviewResponse对象
final previewResponse = ImportPreviewResponse.fromJson(responseData);
emit(NovelImportPreviewReady(
previewResponse: previewResponse,
fileName: event.fileName,
));
} catch (e) {
AppLogger.e('NovelImportBloc', '获取导入预览失败', e);
emit(NovelImportFailure(message: '获取预览失败: ${e.toString()}'));
}
}
/// 处理确认并开始导入事件
Future<void> _onConfirmAndStartImport(
ConfirmAndStartImport event, Emitter<NovelImportState> emit) async {
emit(NovelImportInProgress(status: 'CONFIRMING', message: '确认导入配置...'));
try {
// 确认并开始导入
final jobId = await novelRepository.confirmAndStartImport(
previewSessionId: event.previewSessionId,
finalTitle: event.finalTitle,
selectedChapterIndexes: event.selectedChapterIndexes,
enableSmartContext: event.enableSmartContext,
enableAISummary: event.enableAISummary,
aiConfigId: event.aiConfigId,
);
emit(NovelImportInProgress(
status: 'PROCESSING', message: '开始处理...', jobId: jobId));
// 订阅导入状态更新
_importStatusSubscription?.cancel();
_importStatusSubscription = novelRepository.getImportStatus(jobId).listen(
(importStatus) {
add(ImportStatusUpdate(
status: importStatus.status,
message: importStatus.message,
jobId: jobId,
progress: importStatus.progress,
currentStep: importStatus.currentStep,
processedChapters: importStatus.processedChapters,
totalChapters: importStatus.totalChapters,
));
},
onError: (error) {
AppLogger.e('NovelImportBloc', '监听导入状态流错误', error);
add(ImportStatusUpdate(
status: 'FAILED',
message: '监听导入状态失败: ${error.toString()}',
jobId: jobId,
));
},
onDone: () {
AppLogger.i('NovelImportBloc', '导入状态流已关闭');
},
);
} catch (e) {
AppLogger.e('NovelImportBloc', '确认导入失败', e);
emit(NovelImportFailure(message: '确认导入失败: ${e.toString()}'));
}
}
/// 处理导入状态更新事件
void _onImportStatusUpdate(
ImportStatusUpdate event, Emitter<NovelImportState> emit) {
if (event.status == 'COMPLETED') {
emit(NovelImportSuccess(message: event.message));
_importStatusSubscription?.cancel();
_importStatusSubscription = null;
} else if (event.status == 'FAILED' || event.status == 'ERROR') {
emit(NovelImportFailure(message: event.message));
_importStatusSubscription?.cancel();
_importStatusSubscription = null;
} else {
emit(NovelImportInProgress(
status: event.status,
message: event.message,
jobId: event.jobId,
progress: event.progress,
currentStep: event.currentStep,
processedChapters: event.processedChapters,
totalChapters: event.totalChapters,
));
}
}
/// 处理清理预览会话事件
Future<void> _onCleanupPreviewSession(
CleanupPreviewSession event, Emitter<NovelImportState> emit) async {
try {
await novelRepository.cleanupPreviewSession(event.previewSessionId);
AppLogger.i('NovelImportBloc', '预览会话已清理: ${event.previewSessionId}');
} catch (e) {
AppLogger.e('NovelImportBloc', '清理预览会话失败', e);
}
}
/// 重置导入状态
void _onResetImportState(
ResetImportState event, Emitter<NovelImportState> emit) async {
try {
// 如果已经不是InProgress状态不再重复取消
if (state is! NovelImportInProgress) {
emit(NovelImportInitial());
return;
}
// 记录当前JobId避免重复取消
final currentState = state as NovelImportInProgress;
final jobId = currentState.jobId;
// 立即切换到取消中状态,防止重复操作
emit(NovelImportInProgress(
status: 'CANCELLING',
message: '正在取消导入...',
jobId: jobId
));
// 取消订阅
await _importStatusSubscription?.cancel();
_importStatusSubscription = null;
// 如果有JobId尝试取消任务
if (jobId != null) {
// 通知服务器取消任务
final success = await novelRepository.cancelImport(jobId);
AppLogger.i('NovelImportBloc',
'导入任务取消${success ? '成功' : '失败或已完成'}: $jobId');
}
// 重置状态
emit(NovelImportInitial());
} catch (e) {
AppLogger.e('NovelImportBloc', '重置导入状态时出错', e);
// 即使出错,也要确保状态被重置
emit(NovelImportInitial());
}
}
/// 处理传统导入小说文件事件(向后兼容)
Future<void> _onImportNovelFile(
ImportNovelFile event, Emitter<NovelImportState> emit) async {
emit(NovelImportInProgress(status: 'PREPARING', message: '准备中...'));
try {
// 选择文件
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['txt'],
withData: true,
);
if (result == null || result.files.isEmpty) {
emit(NovelImportInitial());
return;
}
final file = result.files.first;
final fileBytes = file.bytes;
final fileName = file.name;
if (fileBytes == null) {
emit(NovelImportFailure(message: '无法读取文件数据'));
return;
}
emit(NovelImportInProgress(status: 'UPLOADING', message: '上传中...'));
// 上传文件并获取任务ID
final jobId = await novelRepository.importNovel(fileBytes, fileName);
emit(NovelImportInProgress(
status: 'PROCESSING', message: '处理中...', jobId: jobId));
// 订阅导入状态更新
_importStatusSubscription?.cancel();
_importStatusSubscription = novelRepository.getImportStatus(jobId).listen(
(importStatus) {
add(ImportStatusUpdate(
status: importStatus.status,
message: importStatus.message,
jobId: jobId,
));
},
onError: (error) {
AppLogger.e('NovelImportBloc', '监听导入状态流错误', error);
add(ImportStatusUpdate(
status: 'FAILED',
message: '监听导入状态失败: ${error.toString()}',
jobId: jobId,
));
},
onDone: () {
AppLogger.i('NovelImportBloc', '导入状态流已关闭');
},
);
} catch (e) {
AppLogger.e('NovelImportBloc', '导入小说失败', e);
emit(NovelImportFailure(message: '导入失败: ${e.toString()}'));
}
}
@override
Future<void> close() {
_importStatusSubscription?.cancel();
return super.close();
}
}

View File

@@ -0,0 +1,132 @@
part of 'novel_import_bloc.dart';
/// 小说导入事件基类
abstract class NovelImportEvent extends Equatable {
const NovelImportEvent();
@override
List<Object?> get props => [];
}
/// 第一步:上传文件获取预览
class UploadFileForPreview extends NovelImportEvent {
const UploadFileForPreview();
}
/// 第二步:获取导入预览
class GetImportPreview extends NovelImportEvent {
const GetImportPreview({
required this.previewSessionId,
this.customTitle,
this.chapterLimit,
this.enableSmartContext = true,
this.enableAISummary = false,
this.aiConfigId,
this.previewChapterCount = 10,
required this.fileName,
});
final String previewSessionId;
final String? customTitle;
final int? chapterLimit;
final bool enableSmartContext;
final bool enableAISummary;
final String? aiConfigId;
final int previewChapterCount;
final String fileName;
@override
List<Object?> get props => [
previewSessionId,
customTitle,
chapterLimit,
enableSmartContext,
enableAISummary,
aiConfigId,
previewChapterCount,
fileName,
];
}
/// 第三步:确认并开始导入
class ConfirmAndStartImport extends NovelImportEvent {
const ConfirmAndStartImport({
required this.previewSessionId,
required this.finalTitle,
this.selectedChapterIndexes,
this.enableSmartContext = true,
this.enableAISummary = false,
this.aiConfigId,
});
final String previewSessionId;
final String finalTitle;
final List<int>? selectedChapterIndexes;
final bool enableSmartContext;
final bool enableAISummary;
final String? aiConfigId;
@override
List<Object?> get props => [
previewSessionId,
finalTitle,
selectedChapterIndexes,
enableSmartContext,
enableAISummary,
aiConfigId,
];
}
/// 导入状态更新事件
class ImportStatusUpdate extends NovelImportEvent {
const ImportStatusUpdate({
required this.status,
required this.message,
required this.jobId,
this.progress,
this.currentStep,
this.processedChapters,
this.totalChapters,
});
final String status;
final String message;
final String jobId;
final double? progress;
final String? currentStep;
final int? processedChapters;
final int? totalChapters;
@override
List<Object?> get props => [
status,
message,
jobId,
progress,
currentStep,
processedChapters,
totalChapters,
];
}
/// 重置导入状态
class ResetImportState extends NovelImportEvent {
const ResetImportState();
}
/// 清理预览会话
class CleanupPreviewSession extends NovelImportEvent {
const CleanupPreviewSession({
required this.previewSessionId,
});
final String previewSessionId;
@override
List<Object?> get props => [previewSessionId];
}
/// 传统导入小说文件事件(向后兼容)
class ImportNovelFile extends NovelImportEvent {
const ImportNovelFile();
}

View File

@@ -0,0 +1,231 @@
part of 'novel_import_bloc.dart';
/// 小说导入状态基类
abstract class NovelImportState extends Equatable {
const NovelImportState();
@override
List<Object?> get props => [];
}
/// 初始状态
class NovelImportInitial extends NovelImportState {}
/// 第一步:上传文件中
class NovelImportUploading extends NovelImportState {
const NovelImportUploading({required this.message});
final String message;
@override
List<Object?> get props => [message];
}
/// 第一步完成:文件已上传
class NovelImportFileUploaded extends NovelImportState {
const NovelImportFileUploaded({
required this.previewSessionId,
required this.fileName,
required this.fileSize,
});
final String previewSessionId;
final String fileName;
final int fileSize;
@override
List<Object?> get props => [previewSessionId, fileName, fileSize];
}
/// 第二步:加载预览中
class NovelImportLoadingPreview extends NovelImportState {
const NovelImportLoadingPreview({required this.message});
final String message;
@override
List<Object?> get props => [message];
}
/// 第二步完成:预览准备就绪
class NovelImportPreviewReady extends NovelImportState {
const NovelImportPreviewReady({
required this.previewResponse,
required this.fileName,
});
final ImportPreviewResponse previewResponse;
final String fileName;
@override
List<Object?> get props => [previewResponse, fileName];
}
/// 第三步:导入进行中
class NovelImportInProgress extends NovelImportState {
const NovelImportInProgress({
required this.status,
required this.message,
this.jobId,
this.progress,
this.currentStep,
this.processedChapters,
this.totalChapters,
});
final String status;
final String message;
final String? jobId;
final double? progress;
final String? currentStep;
final int? processedChapters;
final int? totalChapters;
@override
List<Object?> get props => [
status,
message,
jobId,
progress,
currentStep,
processedChapters,
totalChapters,
];
}
/// 导入成功
class NovelImportSuccess extends NovelImportState {
const NovelImportSuccess({required this.message});
final String message;
@override
List<Object?> get props => [message];
}
/// 导入失败
class NovelImportFailure extends NovelImportState {
const NovelImportFailure({required this.message});
final String message;
@override
List<Object?> get props => [message];
}
/// 导入预览响应数据类
class ImportPreviewResponse {
const ImportPreviewResponse({
required this.previewSessionId,
required this.detectedTitle,
required this.totalChapterCount,
required this.chapterPreviews,
required this.totalWordCount,
this.aiEstimation,
this.warnings = const [],
});
final String previewSessionId;
final String detectedTitle;
final int totalChapterCount;
final List<ChapterPreview> chapterPreviews;
final int totalWordCount;
final AIEstimation? aiEstimation;
final List<String> warnings;
factory ImportPreviewResponse.fromJson(Map<String, dynamic> json) {
return ImportPreviewResponse(
previewSessionId: json['previewSessionId'] as String,
detectedTitle: json['detectedTitle'] as String,
totalChapterCount: json['totalChapterCount'] as int,
chapterPreviews: (json['chapterPreviews'] as List<dynamic>)
.map((e) => ChapterPreview.fromJson(e as Map<String, dynamic>))
.toList(),
totalWordCount: json['totalWordCount'] as int,
aiEstimation: json['aiEstimation'] != null
? AIEstimation.fromJson(json['aiEstimation'] as Map<String, dynamic>)
: null,
warnings: json['warnings'] != null
? List<String>.from(json['warnings'] as List<dynamic>)
: const [],
);
}
}
/// 章节预览数据类
class ChapterPreview {
const ChapterPreview({
required this.chapterIndex,
required this.title,
required this.contentPreview,
required this.fullContentLength,
required this.wordCount,
this.selected = true,
});
final int chapterIndex;
final String title;
final String contentPreview;
final int fullContentLength;
final int wordCount;
final bool selected;
factory ChapterPreview.fromJson(Map<String, dynamic> json) {
return ChapterPreview(
chapterIndex: json['chapterIndex'] as int,
title: json['title'] as String,
contentPreview: json['contentPreview'] as String,
fullContentLength: json['fullContentLength'] as int,
wordCount: json['wordCount'] as int,
selected: json['selected'] as bool? ?? true,
);
}
ChapterPreview copyWith({
int? chapterIndex,
String? title,
String? contentPreview,
int? fullContentLength,
int? wordCount,
bool? selected,
}) {
return ChapterPreview(
chapterIndex: chapterIndex ?? this.chapterIndex,
title: title ?? this.title,
contentPreview: contentPreview ?? this.contentPreview,
fullContentLength: fullContentLength ?? this.fullContentLength,
wordCount: wordCount ?? this.wordCount,
selected: selected ?? this.selected,
);
}
}
/// AI估算数据类
class AIEstimation {
const AIEstimation({
required this.supported,
this.estimatedTokens,
this.estimatedCost,
this.estimatedTimeMinutes,
this.selectedModel,
this.limitations,
});
final bool supported;
final int? estimatedTokens;
final double? estimatedCost;
final int? estimatedTimeMinutes;
final String? selectedModel;
final String? limitations;
factory AIEstimation.fromJson(Map<String, dynamic> json) {
return AIEstimation(
supported: json['supported'] as bool,
estimatedTokens: json['estimatedTokens'] as int?,
estimatedCost: json['estimatedCost'] as double?,
estimatedTimeMinutes: json['estimatedTimeMinutes'] as int?,
selectedModel: json['selectedModel'] as String?,
limitations: json['limitations'] as String?,
);
}
}

View File

@@ -0,0 +1,400 @@
import 'package:ainoval/models/novel_summary.dart';
import 'package:ainoval/services/api_service/repositories/novel_repository.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// 事件定义
abstract class NovelListEvent extends Equatable {
@override
List<Object?> get props => [];
}
class LoadNovels extends NovelListEvent {}
class SearchNovels extends NovelListEvent {
SearchNovels({required this.query});
final String query;
@override
List<Object?> get props => [query];
}
class FilterNovels extends NovelListEvent {
FilterNovels({required this.filterOption});
final FilterOption filterOption;
@override
List<Object?> get props => [filterOption];
}
class SortNovels extends NovelListEvent {
SortNovels({required this.sortOption});
final SortOption sortOption;
@override
List<Object?> get props => [sortOption];
}
class GroupNovels extends NovelListEvent {
GroupNovels({required this.groupOption});
final GroupOption groupOption;
@override
List<Object?> get props => [groupOption];
}
class DeleteNovel extends NovelListEvent {
DeleteNovel({required this.id});
final String id;
@override
List<Object?> get props => [id];
}
// 添加创建小说的事件
class CreateNovel extends NovelListEvent {
CreateNovel({
required this.title,
this.seriesName,
});
final String title;
final String? seriesName;
@override
List<Object?> get props => [title, seriesName];
}
// 状态定义
abstract class NovelListState extends Equatable {
@override
List<Object?> get props => [];
}
class NovelListInitial extends NovelListState {}
class NovelListLoading extends NovelListState {}
class NovelListLoaded extends NovelListState {
NovelListLoaded({
required List<NovelSummary> allNovels,
this.sortOption = SortOption.lastEdited,
this.filterOption = const FilterOption(),
this.groupOption = GroupOption.none,
this.searchQuery = '',
}) : _allNovels = allNovels,
novels = _applySearchAndFilterAndSort(allNovels, searchQuery, filterOption, sortOption);
final List<NovelSummary> _allNovels;
final List<NovelSummary> novels;
final SortOption sortOption;
final FilterOption filterOption;
final GroupOption groupOption;
final String searchQuery;
@override
List<Object?> get props => [_allNovels, novels, sortOption, filterOption, groupOption, searchQuery];
static List<NovelSummary> _applySearchAndFilterAndSort(
List<NovelSummary> novels,
String searchQuery,
FilterOption filterOption,
SortOption sortOption,
) {
List<NovelSummary> processedNovels = List.from(novels);
if (searchQuery.isNotEmpty) {
processedNovels = processedNovels.where((novel) {
final titleMatch = novel.title.toLowerCase().contains(searchQuery.toLowerCase());
final seriesMatch = novel.seriesName.toLowerCase().contains(searchQuery.toLowerCase());
return titleMatch || seriesMatch;
}).toList();
}
if (filterOption.series != null && filterOption.series!.isNotEmpty) {
processedNovels = processedNovels.where((novel) {
return novel.seriesName.toLowerCase() == filterOption.series!.toLowerCase();
}).toList();
}
switch (sortOption) {
case SortOption.lastEdited:
processedNovels.sort((a, b) => b.lastEditTime.compareTo(a.lastEditTime));
break;
case SortOption.title:
processedNovels.sort((a, b) => a.title.compareTo(b.title));
break;
case SortOption.wordCount:
processedNovels.sort((a, b) => b.wordCount.compareTo(a.wordCount));
break;
case SortOption.creationDate:
processedNovels.sort((a, b) => b.lastEditTime.compareTo(a.lastEditTime));
break;
case SortOption.actCount:
processedNovels.sort((a, b) => b.actCount.compareTo(a.actCount));
break;
case SortOption.chapterCount:
processedNovels.sort((a, b) => b.chapterCount.compareTo(a.chapterCount));
break;
case SortOption.sceneCount:
processedNovels.sort((a, b) => b.sceneCount.compareTo(a.sceneCount));
break;
}
return processedNovels;
}
NovelListLoaded copyWith({
List<NovelSummary>? allNovels,
SortOption? sortOption,
FilterOption? filterOption,
GroupOption? groupOption,
String? searchQuery,
}) {
return NovelListLoaded(
allNovels: allNovels ?? _allNovels,
sortOption: sortOption ?? this.sortOption,
filterOption: filterOption ?? this.filterOption,
groupOption: groupOption ?? this.groupOption,
searchQuery: searchQuery ?? this.searchQuery,
);
}
}
class NovelListError extends NovelListState {
NovelListError({required this.message});
final String message;
@override
List<Object?> get props => [message];
}
// 排序选项
enum SortOption {
lastEdited,
title,
wordCount,
creationDate,
actCount,
chapterCount,
sceneCount,
}
// 分组选项
enum GroupOption {
none,
series,
status,
}
// 过滤选项
class FilterOption extends Equatable {
const FilterOption({
this.showCompleted = true,
this.showInProgress = true,
this.showNotStarted = true,
this.minWordCount = 0,
this.maxWordCount,
this.series,
});
final bool showCompleted;
final bool showInProgress;
final bool showNotStarted;
final int minWordCount;
final int? maxWordCount;
final String? series;
@override
List<Object?> get props => [
showCompleted,
showInProgress,
showNotStarted,
minWordCount,
maxWordCount,
series,
];
}
// 添加强制刷新事件
class RefreshNovels extends NovelListEvent {
@override
List<Object?> get props => [];
}
// 清理状态事件(用于退出登录)
class ClearNovels extends NovelListEvent {
@override
List<Object?> get props => [];
}
// Bloc实现
class NovelListBloc extends Bloc<NovelListEvent, NovelListState> {
NovelListBloc({required this.repository}) : super(NovelListInitial()) {
on<LoadNovels>(_onLoadNovels);
on<RefreshNovels>(_onRefreshNovels);
on<ClearNovels>(_onClearNovels);
on<SearchNovels>(_onSearchNovels);
on<FilterNovels>(_onFilterNovels);
on<SortNovels>(_onSortNovels);
on<GroupNovels>(_onGroupNovels);
on<DeleteNovel>(_onDeleteNovel);
on<CreateNovel>(_onCreateNovel);
}
final NovelRepository repository;
// 防止重复加载标志
bool _isLoading = false;
// 数据是否已经加载过的标志
bool _hasLoadedData = false;
Future<void> _onLoadNovels(LoadNovels event, Emitter<NovelListState> emit) async {
// 如果数据已经加载过且当前不是错误状态,则不重复加载
if (_hasLoadedData && state is NovelListLoaded) return;
// 如果已经在加载中,则不重复加载
if (_isLoading || state is NovelListLoading) return;
_isLoading = true;
// 只有在没有数据时才显示加载状态
if (!_hasLoadedData) {
emit(NovelListLoading());
}
try {
final novels = await repository.fetchNovels();
// 转换为NovelSummary列表
final novelSummaries = novels.map((novel) => NovelSummary(
id: novel.id,
title: novel.title,
coverUrl: novel.coverUrl,
lastEditTime: novel.updatedAt,
wordCount: novel.wordCount,
readTime: novel.readTime,
version: novel.version,
completionPercentage: 0.0,
lastEditedChapterId: novel.lastEditedChapterId,
author: novel.author?.username,
contributors: novel.contributors,
actCount: novel.getActCount(),
chapterCount: novel.getChapterCount(),
sceneCount: novel.getSceneCount(),
serverUpdatedAt: novel.updatedAt,
)).toList();
_hasLoadedData = true;
emit(NovelListLoaded(allNovels: novelSummaries));
} catch (e) {
emit(NovelListError(message: e.toString()));
} finally {
_isLoading = false;
}
}
// 强制刷新数据(忽略缓存)
Future<void> _onRefreshNovels(RefreshNovels event, Emitter<NovelListState> emit) async {
// 重置缓存标志,强制重新加载
_hasLoadedData = false;
add(LoadNovels());
}
// 清理小说列表状态(用于退出登录)
void _onClearNovels(ClearNovels event, Emitter<NovelListState> emit) {
// 重置所有标志
_isLoading = false;
_hasLoadedData = false;
// 恢复到初始状态
emit(NovelListInitial());
}
Future<void> _onSearchNovels(SearchNovels event, Emitter<NovelListState> emit) async {
final currentState = state;
if (currentState is NovelListLoaded) {
emit(currentState.copyWith(searchQuery: event.query));
}
}
void _onFilterNovels(FilterNovels event, Emitter<NovelListState> emit) {
final currentState = state;
if (currentState is NovelListLoaded) {
emit(currentState.copyWith(filterOption: event.filterOption));
}
}
void _onSortNovels(SortNovels event, Emitter<NovelListState> emit) {
final currentState = state;
if (currentState is NovelListLoaded) {
emit(currentState.copyWith(sortOption: event.sortOption));
}
}
void _onGroupNovels(GroupNovels event, Emitter<NovelListState> emit) {
final currentState = state;
if (currentState is NovelListLoaded) {
emit(currentState.copyWith(groupOption: event.groupOption));
}
}
Future<void> _onDeleteNovel(DeleteNovel event, Emitter<NovelListState> emit) async {
final currentState = state;
if (currentState is NovelListLoaded) {
try {
await repository.deleteNovel(event.id);
final updatedNovels = currentState._allNovels.where((novel) => novel.id != event.id).toList();
emit(currentState.copyWith(allNovels: updatedNovels));
} catch (e) {
emit(NovelListError(message: e.toString()));
}
}
}
// 添加创建小说的处理方法
Future<void> _onCreateNovel(CreateNovel event, Emitter<NovelListState> emit) async {
try {
final newNovel = await repository.createNovel(event.title);
// 将Novel转换为NovelSummary
final novelSummary = NovelSummary(
id: newNovel.id,
title: newNovel.title,
coverUrl: newNovel.coverUrl,
lastEditTime: newNovel.updatedAt,
wordCount: newNovel.wordCount,
readTime: newNovel.readTime,
version: newNovel.version,
seriesName: event.seriesName ?? '',
completionPercentage: 0.0,
author: newNovel.author?.username,
contributors: newNovel.contributors,
actCount: newNovel.getActCount(),
chapterCount: newNovel.getChapterCount(),
sceneCount: newNovel.getSceneCount(),
serverUpdatedAt: newNovel.updatedAt,
);
// 直接更新状态,添加新创建的小说
final currentState = state;
if (currentState is NovelListLoaded) {
final updatedNovels = List<NovelSummary>.from(currentState._allNovels)..add(novelSummary);
emit(currentState.copyWith(allNovels: updatedNovels));
} else {
// 如果当前不是加载状态,则重新加载整个列表
add(LoadNovels());
}
} catch (e) {
emit(NovelListError(message: e.toString()));
}
}
}

View File

@@ -0,0 +1,401 @@
import 'dart:async';
import 'package:ainoval/models/novel_structure.dart' as novel_models;
import 'package:ainoval/services/api_service/repositories/impl/editor_repository_impl.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../utils/logger.dart';
part 'plan_event.dart';
part 'plan_state.dart';
class PlanBloc extends Bloc<PlanEvent, PlanState> {
PlanBloc({
required EditorRepositoryImpl repository,
required this.novelId,
}) : repository = repository,
super(PlanInitial()) {
on<LoadPlanContent>(_onLoadContent);
on<UpdateActTitle>(_onUpdateActTitle);
on<UpdateChapterTitle>(_onUpdateChapterTitle);
on<UpdateSceneSummary>(_onUpdateSceneSummary);
on<AddNewAct>(_onAddNewAct);
on<AddNewChapter>(_onAddNewChapter);
on<AddNewScene>(_onAddNewScene);
on<MoveScene>(_onMoveScene);
on<DeleteScene>(_onDeleteScene);
}
final EditorRepositoryImpl repository;
final String novelId;
Future<void> _onLoadContent(
LoadPlanContent event, Emitter<PlanState> emit) async {
emit(PlanLoading());
try {
AppLogger.i('PlanBloc/_onLoadContent', '开始加载小说大纲数据');
// 获取小说数据(带场景摘要)
final novel = await repository.getNovelWithSceneSummaries(novelId);
if (novel == null) {
emit(const PlanError(message: '无法加载小说大纲数据'));
return;
}
emit(PlanLoaded(
novel: novel,
isDirty: false,
isSaving: false,
));
} catch (e) {
emit(PlanError(message: e.toString()));
}
}
Future<void> _onUpdateActTitle(
UpdateActTitle event, Emitter<PlanState> emit) async {
final currentState = state;
if (currentState is PlanLoaded) {
try {
// 更新标题逻辑
final acts = currentState.novel.acts.map((act) {
if (act.id == event.actId) {
return act.copyWith(title: event.title);
}
return act;
}).toList();
final updatedNovel = currentState.novel.copyWith(acts: acts);
emit(currentState.copyWith(
novel: updatedNovel,
isDirty: true,
));
// 保存到服务器
await repository.updateActTitle(
novelId,
event.actId,
event.title,
);
emit(currentState.copyWith(isDirty: false));
} catch (e) {
emit(currentState.copyWith(
errorMessage: '更新Act标题失败: ${e.toString()}',
));
}
}
}
Future<void> _onUpdateChapterTitle(
UpdateChapterTitle event, Emitter<PlanState> emit) async {
final currentState = state;
if (currentState is PlanLoaded) {
try {
// 更新标题逻辑
final acts = currentState.novel.acts.map((act) {
if (act.id == event.actId) {
final chapters = act.chapters.map((chapter) {
if (chapter.id == event.chapterId) {
return chapter.copyWith(title: event.title);
}
return chapter;
}).toList();
return act.copyWith(chapters: chapters);
}
return act;
}).toList();
final updatedNovel = currentState.novel.copyWith(acts: acts);
emit(currentState.copyWith(
novel: updatedNovel,
isDirty: true,
));
// 保存到服务器
await repository.updateChapterTitle(
novelId,
event.actId,
event.chapterId,
event.title,
);
emit(currentState.copyWith(isDirty: false));
} catch (e) {
emit(currentState.copyWith(
errorMessage: '更新Chapter标题失败: ${e.toString()}',
));
}
}
}
Future<void> _onUpdateSceneSummary(
UpdateSceneSummary event, Emitter<PlanState> emit) async {
final currentState = state;
if (currentState is PlanLoaded) {
try {
// 更新摘要逻辑
bool updated = false;
final acts = currentState.novel.acts.map((act) {
if (act.id == event.actId) {
final chapters = act.chapters.map((chapter) {
if (chapter.id == event.chapterId) {
final scenes = chapter.scenes.map((scene) {
if (scene.id == event.sceneId) {
updated = true;
final updatedSummary = novel_models.Summary(
id: scene.summary.id,
content: event.summary,
);
return scene.copyWith(summary: updatedSummary);
}
return scene;
}).toList();
return chapter.copyWith(scenes: scenes);
}
return chapter;
}).toList();
return act.copyWith(chapters: chapters);
}
return act;
}).toList();
if (!updated) {
emit(currentState.copyWith(
errorMessage: '未找到对应的场景',
));
return;
}
final updatedNovel = currentState.novel.copyWith(acts: acts);
// 先更新UI以立即反映更改
emit(currentState.copyWith(
novel: updatedNovel,
isDirty: true,
));
// 保存到服务器
await repository.updateSummary(
novelId,
event.actId,
event.chapterId,
event.sceneId,
event.summary,
);
// 只更新isDirty标志保持更新后的novel对象
emit(currentState.copyWith(
novel: updatedNovel,
isDirty: false,
));
} catch (e) {
emit(currentState.copyWith(
errorMessage: '更新场景摘要失败: ${e.toString()}',
));
}
}
}
Future<void> _onAddNewAct(
AddNewAct event, Emitter<PlanState> emit) async {
final currentState = state;
if (currentState is PlanLoaded) {
try {
emit(currentState.copyWith(isSaving: true));
// 调用API创建新Act
final updatedNovel = await repository.addNewAct(
novelId,
event.title,
);
if (updatedNovel == null) {
emit(currentState.copyWith(
isSaving: false,
errorMessage: '添加新Act失败',
));
return;
}
emit(currentState.copyWith(
novel: updatedNovel,
isSaving: false,
));
} catch (e) {
emit(currentState.copyWith(
isSaving: false,
errorMessage: '添加新Act失败: ${e.toString()}',
));
}
}
}
Future<void> _onAddNewChapter(
AddNewChapter event, Emitter<PlanState> emit) async {
final currentState = state;
if (currentState is PlanLoaded) {
try {
emit(currentState.copyWith(isSaving: true));
// 调用API创建新Chapter
final updatedNovel = await repository.addNewChapter(
novelId,
event.actId,
event.title,
);
if (updatedNovel == null) {
emit(currentState.copyWith(
isSaving: false,
errorMessage: '添加新Chapter失败',
));
return;
}
emit(currentState.copyWith(
novel: updatedNovel,
isSaving: false,
));
} catch (e) {
emit(currentState.copyWith(
isSaving: false,
errorMessage: '添加新Chapter失败: ${e.toString()}',
));
}
}
}
Future<void> _onAddNewScene(
AddNewScene event, Emitter<PlanState> emit) async {
final currentState = state;
if (currentState is PlanLoaded) {
try {
emit(currentState.copyWith(isSaving: true));
// 调用API创建新Scene
final updatedNovel = await repository.addNewScene(
novelId,
event.actId,
event.chapterId,
);
if (updatedNovel == null) {
emit(currentState.copyWith(
isSaving: false,
errorMessage: '添加新Scene失败',
));
return;
}
emit(currentState.copyWith(
novel: updatedNovel,
isSaving: false,
));
} catch (e) {
emit(currentState.copyWith(
isSaving: false,
errorMessage: '添加新Scene失败: ${e.toString()}',
));
}
}
}
Future<void> _onMoveScene(
MoveScene event, Emitter<PlanState> emit) async {
final currentState = state;
if (currentState is PlanLoaded) {
try {
emit(currentState.copyWith(isSaving: true));
// 调用API移动Scene
final updatedNovel = await repository.moveScene(
novelId,
event.sourceActId,
event.sourceChapterId,
event.sourceSceneId,
event.targetActId,
event.targetChapterId,
event.targetIndex,
);
if (updatedNovel == null) {
emit(currentState.copyWith(
isSaving: false,
errorMessage: '移动场景失败',
));
return;
}
emit(currentState.copyWith(
novel: updatedNovel,
isSaving: false,
));
} catch (e) {
emit(currentState.copyWith(
isSaving: false,
errorMessage: '移动场景失败: ${e.toString()}',
));
}
}
}
Future<void> _onDeleteScene(
DeleteScene event, Emitter<PlanState> emit) async {
final currentState = state;
if (currentState is PlanLoaded) {
try {
emit(currentState.copyWith(isSaving: true));
// 调用API删除场景
final success = await repository.deleteScene(
novelId,
event.actId,
event.chapterId,
event.sceneId,
);
if (!success) {
emit(currentState.copyWith(
isSaving: false,
errorMessage: '删除场景失败',
));
return;
}
// 从小说结构中删除场景
final updatedActs = currentState.novel.acts.map((act) {
if (act.id == event.actId) {
final updatedChapters = act.chapters.map((chapter) {
if (chapter.id == event.chapterId) {
final updatedScenes = chapter.scenes
.where((scene) => scene.id != event.sceneId)
.toList();
return chapter.copyWith(scenes: updatedScenes);
}
return chapter;
}).toList();
return act.copyWith(chapters: updatedChapters);
}
return act;
}).toList();
final updatedNovel = currentState.novel.copyWith(acts: updatedActs);
emit(currentState.copyWith(
novel: updatedNovel,
isSaving: false,
));
} catch (e) {
emit(currentState.copyWith(
isSaving: false,
errorMessage: '删除场景失败: ${e.toString()}',
));
}
}
}
}

View File

@@ -0,0 +1,138 @@
part of 'plan_bloc.dart';
abstract class PlanEvent extends Equatable {
const PlanEvent();
@override
List<Object?> get props => [];
}
class LoadPlanContent extends PlanEvent {
const LoadPlanContent();
}
class UpdateActTitle extends PlanEvent {
const UpdateActTitle({
required this.actId,
required this.title,
});
final String actId;
final String title;
@override
List<Object?> get props => [actId, title];
}
class UpdateChapterTitle extends PlanEvent {
const UpdateChapterTitle({
required this.actId,
required this.chapterId,
required this.title,
});
final String actId;
final String chapterId;
final String title;
@override
List<Object?> get props => [actId, chapterId, title];
}
class UpdateSceneSummary extends PlanEvent {
const UpdateSceneSummary({
required this.novelId,
required this.actId,
required this.chapterId,
required this.sceneId,
required this.summary,
});
final String novelId;
final String actId;
final String chapterId;
final String sceneId;
final String summary;
@override
List<Object?> get props => [novelId, actId, chapterId, sceneId, summary];
}
class AddNewAct extends PlanEvent {
const AddNewAct({this.title = '新Act'});
final String title;
@override
List<Object?> get props => [title];
}
class AddNewChapter extends PlanEvent {
const AddNewChapter({
required this.novelId,
required this.actId,
this.title = '新章节',
});
final String novelId;
final String actId;
final String title;
@override
List<Object?> get props => [novelId, actId, title];
}
class AddNewScene extends PlanEvent {
const AddNewScene({
required this.novelId,
required this.actId,
required this.chapterId,
});
final String novelId;
final String actId;
final String chapterId;
@override
List<Object?> get props => [novelId, actId, chapterId];
}
class MoveScene extends PlanEvent {
const MoveScene({
required this.novelId,
required this.sourceActId,
required this.sourceChapterId,
required this.sourceSceneId,
required this.targetActId,
required this.targetChapterId,
required this.targetIndex,
});
final String novelId;
final String sourceActId;
final String sourceChapterId;
final String sourceSceneId;
final String targetActId;
final String targetChapterId;
final int targetIndex;
@override
List<Object?> get props => [
novelId,
sourceActId,
sourceChapterId,
sourceSceneId,
targetActId,
targetChapterId,
targetIndex,
];
}
class DeleteScene extends PlanEvent {
const DeleteScene({
required this.novelId,
required this.actId,
required this.chapterId,
required this.sceneId,
});
final String novelId;
final String actId;
final String chapterId;
final String sceneId;
@override
List<Object?> get props => [novelId, actId, chapterId, sceneId];
}

View File

@@ -0,0 +1,61 @@
part of 'plan_bloc.dart';
abstract class PlanState extends Equatable {
const PlanState();
@override
List<Object?> get props => [];
}
class PlanInitial extends PlanState {}
class PlanLoading extends PlanState {}
class PlanLoaded extends PlanState {
const PlanLoaded({
required this.novel,
this.isDirty = false,
this.isSaving = false,
this.lastSaveTime,
this.errorMessage,
});
final novel_models.Novel novel;
final bool isDirty;
final bool isSaving;
final DateTime? lastSaveTime;
final String? errorMessage;
@override
List<Object?> get props => [
novel,
isDirty,
isSaving,
lastSaveTime,
errorMessage,
];
PlanLoaded copyWith({
novel_models.Novel? novel,
bool? isDirty,
bool? isSaving,
DateTime? lastSaveTime,
String? errorMessage,
}) {
return PlanLoaded(
novel: novel ?? this.novel,
isDirty: isDirty ?? this.isDirty,
isSaving: isSaving ?? this.isSaving,
lastSaveTime: lastSaveTime ?? this.lastSaveTime,
errorMessage: errorMessage ?? this.errorMessage,
);
}
}
class PlanError extends PlanState {
const PlanError({required this.message});
final String message;
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,996 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ainoval/blocs/preset/preset_event.dart';
import 'package:ainoval/blocs/preset/preset_state.dart';
import 'package:ainoval/services/api_service/repositories/preset_aggregation_repository.dart';
import 'package:ainoval/services/api_service/repositories/ai_preset_repository.dart';
import 'package:ainoval/models/preset_models.dart';
import 'package:ainoval/utils/logger.dart';
/// 预设管理BLoC
/// 负责处理预设相关的业务逻辑和状态管理
class PresetBloc extends Bloc<PresetEvent, PresetState> {
static const String _tag = 'PresetBloc';
final PresetAggregationRepository _aggregationRepository;
final AIPresetRepository _presetRepository;
PresetBloc({
required PresetAggregationRepository aggregationRepository,
required AIPresetRepository presetRepository,
}) : _aggregationRepository = aggregationRepository,
_presetRepository = presetRepository,
super(const PresetState.initial()) {
on<LoadUserPresetOverview>(_onLoadUserPresetOverview);
on<LoadPresetPackage>(_onLoadPresetPackage);
on<LoadBatchPresetPackages>(_onLoadBatchPresetPackages);
on<LoadGroupedPresets>(_onLoadGroupedPresets);
on<LoadAllPresetData>(_onLoadAllPresetData);
on<AddPresetToCache>(_onAddPresetToCache);
on<SelectPreset>(_onSelectPreset);
on<CreatePreset>(_onCreatePreset);
on<OverwritePreset>(_onOverwritePreset);
on<UpdatePreset>(_onUpdatePreset);
on<DeletePreset>(_onDeletePreset);
on<DuplicatePreset>(_onDuplicatePreset);
on<TogglePresetFavorite>(_onTogglePresetFavorite);
on<TogglePresetQuickAccess>(_onTogglePresetQuickAccess);
on<SearchPresets>(_onSearchPresets);
on<ClearPresetSearch>(_onClearPresetSearch);
on<RefreshPresetData>(_onRefreshPresetData);
on<WarmupPresetCache>(_onWarmupPresetCache);
}
/// 加载用户预设概览
Future<void> _onLoadUserPresetOverview(
LoadUserPresetOverview event,
Emitter<PresetState> emit,
) async {
try {
emit(state.copyWith(isLoading: true, errorMessage: null));
final overview = await _aggregationRepository.getUserPresetOverview();
emit(state.copyWith(
isLoading: false,
userOverview: overview,
));
AppLogger.i(_tag, '用户预设概览加载成功');
} catch (e) {
AppLogger.e(_tag, '加载用户预设概览失败', e);
emit(state.copyWith(
isLoading: false,
errorMessage: '加载用户预设概览失败: ${e.toString()}',
));
}
}
/// 加载预设包
Future<void> _onLoadPresetPackage(
LoadPresetPackage event,
Emitter<PresetState> emit,
) async {
try {
emit(state.copyWith(isLoading: true, errorMessage: null));
final package = await _aggregationRepository.getCompletePresetPackage(
event.featureType,
novelId: event.novelId,
);
emit(state.copyWith(
isLoading: false,
currentPackage: package,
));
AppLogger.i(_tag, '预设包加载成功: ${event.featureType}');
} catch (e) {
AppLogger.e(_tag, '加载预设包失败: ${event.featureType}', e);
emit(state.copyWith(
isLoading: false,
errorMessage: '加载预设包失败: ${e.toString()}',
));
}
}
/// 加载批量预设包
Future<void> _onLoadBatchPresetPackages(
LoadBatchPresetPackages event,
Emitter<PresetState> emit,
) async {
try {
emit(state.copyWith(isLoading: true, errorMessage: null));
final packages = await _aggregationRepository.getBatchPresetPackages(
featureTypes: event.featureTypes,
novelId: event.novelId,
);
emit(state.copyWith(
isLoading: false,
batchPackages: packages,
));
AppLogger.i(_tag, '批量预设包加载成功: ${packages.length}');
} catch (e) {
AppLogger.e(_tag, '加载批量预设包失败', e);
emit(state.copyWith(
isLoading: false,
errorMessage: '加载批量预设包失败: ${e.toString()}',
));
}
}
/// 加载分组预设
Future<void> _onLoadGroupedPresets(
LoadGroupedPresets event,
Emitter<PresetState> emit,
) async {
try {
emit(state.copyWith(isLoading: true, errorMessage: null));
final groupedPresets = await _presetRepository.getUserPresetsByFeatureType(
userId: event.userId,
);
// 加载系统预设并合并
final systemPresets = await _presetRepository.getSystemPresets();
// 合并系统预设到分组中
final mergedGroupedPresets = Map<String, List<AIPromptPreset>>.from(groupedPresets);
for (final preset in systemPresets) {
final featureType = preset.aiFeatureType;
if (!mergedGroupedPresets.containsKey(featureType)) {
mergedGroupedPresets[featureType] = [];
}
mergedGroupedPresets[featureType]!.insert(0, preset);
}
emit(state.copyWith(
isLoading: false,
groupedPresets: mergedGroupedPresets,
));
AppLogger.i(_tag, '分组预设加载成功: ${mergedGroupedPresets.length} 个分组');
} catch (e) {
AppLogger.e(_tag, '加载分组预设失败', e);
emit(state.copyWith(
isLoading: false,
errorMessage: '加载分组预设失败: ${e.toString()}',
));
}
}
/// 选择预设
Future<void> _onSelectPreset(
SelectPreset event,
Emitter<PresetState> emit,
) async {
try {
// 🚀 修复:优先从已加载的聚合数据中查找预设,避免重复请求后端
AIPromptPreset? preset;
if (state.allPresetData != null) {
// 从聚合数据的所有预设中查找
preset = state.allPresetData!.allPresets
.where((p) => p.presetId == event.presetId)
.firstOrNull;
if (preset != null) {
AppLogger.i(_tag, '✅ 从聚合数据中找到预设: ${event.presetId}');
}
}
// 如果聚合数据中没有找到,尝试从分组预设中查找
if (preset == null && state.groupedPresets.isNotEmpty) {
for (final presets in state.groupedPresets.values) {
preset = presets
.where((p) => p.presetId == event.presetId)
.firstOrNull;
if (preset != null) {
AppLogger.i(_tag, '✅ 从分组预设中找到预设: ${event.presetId}');
break;
}
}
}
// 最后的回退:如果缓存中都没有,才去后端获取
if (preset == null) {
AppLogger.w(_tag, '⚠️ 缓存中未找到预设,从后端获取: ${event.presetId}');
preset = await _presetRepository.getPresetById(event.presetId);
}
emit(state.copyWith(
selectedPreset: preset,
errorMessage: null,
));
AppLogger.i(_tag, '📘 预设选择成功: ${event.presetId}');
} catch (e) {
AppLogger.e(_tag, '选择预设失败: ${event.presetId}', e);
emit(state.copyWith(
errorMessage: '选择预设失败: ${e.toString()}',
));
}
}
/// 创建预设
Future<void> _onCreatePreset(
CreatePreset event,
Emitter<PresetState> emit,
) async {
try {
emit(state.copyWith(isLoading: true, errorMessage: null));
final newPreset = await _presetRepository.createPreset(event.request);
// 🚀 优化直接更新本地状态不重新请求API
final updatedGroupedPresets = Map<String, List<AIPromptPreset>>.from(state.groupedPresets);
final newFeatureType = newPreset.aiFeatureType;
// 🚀 修复:处理功能类型格式不一致问题
// 先查找是否存在相同功能类型的其他格式键
String? existingKey = _findExistingFeatureTypeKey(updatedGroupedPresets, newFeatureType);
final targetKey = existingKey ?? newFeatureType;
if (updatedGroupedPresets.containsKey(targetKey)) {
// 将新预设添加到对应功能类型的列表开头
updatedGroupedPresets[targetKey] = [newPreset, ...updatedGroupedPresets[targetKey]!];
} else {
// 如果该功能类型还没有预设,创建新列表
updatedGroupedPresets[targetKey] = [newPreset];
}
AppLogger.i(_tag, '📋 预设添加到分组: $targetKey (原始类型: $newFeatureType)');
// 🚀 新增:同时更新聚合数据缓存
final newAllPresetData = state.allPresetData != null
? _addPresetToAggregatedData(state.allPresetData!, newPreset)
: null;
emit(state.copyWith(
isLoading: false,
selectedPreset: newPreset,
groupedPresets: updatedGroupedPresets,
allPresetData: newAllPresetData,
));
AppLogger.i(_tag, '📘 预设创建成功: ${newPreset.presetId}');
} catch (e) {
AppLogger.e(_tag, '❌ 创建预设失败', e);
emit(state.copyWith(
isLoading: false,
errorMessage: '创建预设失败: ${e.toString()}',
));
}
}
/// 覆盖更新预设(完整对象)
Future<void> _onOverwritePreset(
OverwritePreset event,
Emitter<PresetState> emit,
) async {
try {
emit(state.copyWith(isLoading: true, errorMessage: null));
final updatedPreset = await _presetRepository.overwritePreset(event.preset);
// 🚀 直接更新本地缓存
final updatedGroupedPresets = Map<String, List<AIPromptPreset>>.from(state.groupedPresets);
final newFeatureType = updatedPreset.aiFeatureType;
String? existingKey = _findExistingFeatureTypeKey(updatedGroupedPresets, newFeatureType);
final targetKey = existingKey ?? newFeatureType;
if (updatedGroupedPresets.containsKey(targetKey)) {
final presetList = updatedGroupedPresets[targetKey]!;
final index = presetList.indexWhere((p) => p.presetId == updatedPreset.presetId);
if (index != -1) {
presetList[index] = updatedPreset;
}
}
// 🚀 同时更新聚合数据缓存
final newAllPresetData = _replacePresetInAggregatedData(state.allPresetData, updatedPreset);
emit(state.copyWith(
isLoading: false,
selectedPreset: updatedPreset,
groupedPresets: updatedGroupedPresets,
allPresetData: newAllPresetData,
));
AppLogger.i(_tag, '📘 预设覆盖更新成功: ${updatedPreset.presetId}');
} catch (e) {
AppLogger.e(_tag, '❌ 覆盖更新预设失败: ${event.preset.presetId}', e);
emit(state.copyWith(
isLoading: false,
errorMessage: '覆盖更新预设失败: ${e.toString()}',
));
}
}
/// 更新预设
Future<void> _onUpdatePreset(
UpdatePreset event,
Emitter<PresetState> emit,
) async {
try {
emit(state.copyWith(isLoading: true, errorMessage: null));
AIPromptPreset updatedPreset;
if (event.infoRequest != null) {
updatedPreset = await _presetRepository.updatePresetInfo(
event.presetId,
event.infoRequest!,
);
} else if (event.promptsRequest != null) {
updatedPreset = await _presetRepository.updatePresetPrompts(
event.presetId,
event.promptsRequest!,
);
} else {
throw Exception('更新请求参数错误');
}
// 🚀 优化直接更新本地状态不重新请求API
final updatedGroupedPresets = Map<String, List<AIPromptPreset>>.from(state.groupedPresets);
final newFeatureType = updatedPreset.aiFeatureType;
// 🚀 修复:处理功能类型格式不一致问题
String? existingKey = _findExistingFeatureTypeKey(updatedGroupedPresets, newFeatureType);
final targetKey = existingKey ?? newFeatureType;
if (updatedGroupedPresets.containsKey(targetKey)) {
// 找到并替换对应的预设
final presetList = updatedGroupedPresets[targetKey]!;
final index = presetList.indexWhere((p) => p.presetId == event.presetId);
if (index != -1) {
presetList[index] = updatedPreset;
AppLogger.i(_tag, '📋 预设更新在分组: $targetKey');
}
} else {
AppLogger.w(_tag, '⚠️ 未找到预设分组进行更新: $targetKey');
}
// 🚀 新增:同时更新聚合数据缓存
final newAllPresetData = _replacePresetInAggregatedData(state.allPresetData, updatedPreset);
emit(state.copyWith(
isLoading: false,
selectedPreset: updatedPreset,
groupedPresets: updatedGroupedPresets,
allPresetData: newAllPresetData,
));
AppLogger.i(_tag, '📘 预设更新成功: ${event.presetId}');
} catch (e) {
AppLogger.e(_tag, '❌ 更新预设失败: ${event.presetId}', e);
emit(state.copyWith(
isLoading: false,
errorMessage: '更新预设失败: ${e.toString()}',
));
}
}
/// 删除预设
Future<void> _onDeletePreset(
DeletePreset event,
Emitter<PresetState> emit,
) async {
try {
emit(state.copyWith(isLoading: true, errorMessage: null));
await _presetRepository.deletePreset(event.presetId);
// 🚀 优化直接更新本地状态不重新请求API
final updatedGroupedPresets = Map<String, List<AIPromptPreset>>.from(state.groupedPresets);
// 从所有功能类型的列表中移除该预设
for (final entry in updatedGroupedPresets.entries.toList()) {
final presetList = entry.value;
presetList.removeWhere((p) => p.presetId == event.presetId);
// 如果该功能类型的预设列表为空,移除该分组
if (presetList.isEmpty) {
updatedGroupedPresets.remove(entry.key);
}
}
// 如果删除的是当前选中预设,清除选择
final selectedPreset = state.selectedPreset?.presetId == event.presetId ? null : state.selectedPreset;
// 🚀 新增:同时更新聚合数据缓存
final newAllPresetData = _removePresetFromAggregatedData(state.allPresetData, event.presetId);
emit(state.copyWith(
isLoading: false,
selectedPreset: selectedPreset,
groupedPresets: updatedGroupedPresets,
allPresetData: newAllPresetData,
));
AppLogger.i(_tag, '📘 预设删除成功: ${event.presetId}');
} catch (e) {
AppLogger.e(_tag, '❌ 删除预设失败: ${event.presetId}', e);
emit(state.copyWith(
isLoading: false,
errorMessage: '删除预设失败: ${e.toString()}',
));
}
}
/// 🚀 复制预设
Future<void> _onDuplicatePreset(
DuplicatePreset event,
Emitter<PresetState> emit,
) async {
try {
emit(state.copyWith(isLoading: true, errorMessage: null));
final duplicatedPreset = await _presetRepository.duplicatePreset(event.presetId, event.request);
// 🚀 直接更新本地缓存,类似创建预设的逻辑
final updatedGroupedPresets = Map<String, List<AIPromptPreset>>.from(state.groupedPresets);
final featureType = duplicatedPreset.aiFeatureType;
if (updatedGroupedPresets.containsKey(featureType)) {
// 将复制的预设添加到对应功能类型的列表开头
updatedGroupedPresets[featureType] = [duplicatedPreset, ...updatedGroupedPresets[featureType]!];
} else {
// 如果该功能类型还没有预设,创建新列表
updatedGroupedPresets[featureType] = [duplicatedPreset];
}
// 🚀 同时更新聚合数据缓存
final newAllPresetData = state.allPresetData != null
? _addPresetToAggregatedData(state.allPresetData!, duplicatedPreset)
: null;
emit(state.copyWith(
isLoading: false,
selectedPreset: duplicatedPreset,
groupedPresets: updatedGroupedPresets,
allPresetData: newAllPresetData,
));
AppLogger.i(_tag, '📘 预设复制成功: ${duplicatedPreset.presetId}');
} catch (e) {
AppLogger.e(_tag, '❌ 复制预设失败: ${event.presetId}', e);
emit(state.copyWith(
isLoading: false,
errorMessage: '复制预设失败: ${e.toString()}',
));
}
}
/// 切换预设收藏状态
Future<void> _onTogglePresetFavorite(
TogglePresetFavorite event,
Emitter<PresetState> emit,
) async {
try {
final updatedPreset = await _presetRepository.toggleFavorite(event.presetId);
// 🚀 优化直接更新本地状态不重新请求API
final updatedGroupedPresets = Map<String, List<AIPromptPreset>>.from(state.groupedPresets);
final newFeatureType = updatedPreset.aiFeatureType;
// 🚀 修复:处理功能类型格式不一致问题
String? existingKey = _findExistingFeatureTypeKey(updatedGroupedPresets, newFeatureType);
final targetKey = existingKey ?? newFeatureType;
if (updatedGroupedPresets.containsKey(targetKey)) {
// 找到并替换对应的预设
final presetList = updatedGroupedPresets[targetKey]!;
final index = presetList.indexWhere((p) => p.presetId == event.presetId);
if (index != -1) {
presetList[index] = updatedPreset;
AppLogger.i(_tag, '📋 预设收藏状态更新在分组: $targetKey');
}
} else {
AppLogger.w(_tag, '⚠️ 未找到预设分组进行收藏状态更新: $targetKey');
}
// 更新选中的预设
final selectedPreset = state.selectedPreset?.presetId == event.presetId
? updatedPreset
: state.selectedPreset;
// 🚀 新增:同时更新聚合数据缓存
final newAllPresetData = _replacePresetInAggregatedData(state.allPresetData, updatedPreset);
emit(state.copyWith(
selectedPreset: selectedPreset,
groupedPresets: updatedGroupedPresets,
allPresetData: newAllPresetData,
));
AppLogger.i(_tag, '📘 预设收藏状态切换成功: ${event.presetId}');
} catch (e) {
AppLogger.e(_tag, '❌ 切换预设收藏状态失败: ${event.presetId}', e);
emit(state.copyWith(
errorMessage: '切换收藏状态失败: ${e.toString()}',
));
}
}
/// 切换预设快捷访问状态
Future<void> _onTogglePresetQuickAccess(
TogglePresetQuickAccess event,
Emitter<PresetState> emit,
) async {
try {
final updatedPreset = await _presetRepository.toggleQuickAccess(event.presetId);
// 🚀 优化直接更新本地状态不重新请求API
final updatedGroupedPresets = Map<String, List<AIPromptPreset>>.from(state.groupedPresets);
final newFeatureType = updatedPreset.aiFeatureType;
// 🚀 修复:处理功能类型格式不一致问题
String? existingKey = _findExistingFeatureTypeKey(updatedGroupedPresets, newFeatureType);
final targetKey = existingKey ?? newFeatureType;
if (updatedGroupedPresets.containsKey(targetKey)) {
// 找到并替换对应的预设
final presetList = updatedGroupedPresets[targetKey]!;
final index = presetList.indexWhere((p) => p.presetId == event.presetId);
if (index != -1) {
presetList[index] = updatedPreset;
AppLogger.i(_tag, '📋 预设快捷访问状态更新在分组: $targetKey');
}
} else {
AppLogger.w(_tag, '⚠️ 未找到预设分组进行快捷访问状态更新: $targetKey');
}
// 更新选中的预设
final selectedPreset = state.selectedPreset?.presetId == event.presetId
? updatedPreset
: state.selectedPreset;
// 🚀 新增:同时更新聚合数据缓存
final newAllPresetData = _replacePresetInAggregatedData(state.allPresetData, updatedPreset);
emit(state.copyWith(
selectedPreset: selectedPreset,
groupedPresets: updatedGroupedPresets,
allPresetData: newAllPresetData,
));
AppLogger.i(_tag, '📘 预设快捷访问状态切换成功: ${event.presetId}');
} catch (e) {
AppLogger.e(_tag, '❌ 切换预设快捷访问状态失败: ${event.presetId}', e);
emit(state.copyWith(
errorMessage: '切换快捷访问状态失败: ${e.toString()}',
));
}
}
/// 搜索预设
Future<void> _onSearchPresets(
SearchPresets event,
Emitter<PresetState> emit,
) async {
try {
final searchParams = PresetSearchParams(
keyword: event.query,
featureType: event.featureType,
tags: event.tags,
sortBy: event.sortBy ?? 'recent',
);
final searchResults = await _presetRepository.searchPresets(searchParams);
emit(state.copyWith(
searchResults: searchResults,
searchQuery: event.query,
errorMessage: null,
));
AppLogger.i(_tag, '预设搜索完成: ${searchResults.length} 个结果');
} catch (e) {
AppLogger.e(_tag, '搜索预设失败', e);
emit(state.copyWith(
errorMessage: '搜索预设失败: ${e.toString()}',
));
}
}
/// 清除搜索
Future<void> _onClearPresetSearch(
ClearPresetSearch event,
Emitter<PresetState> emit,
) async {
emit(state.copyWith(
searchResults: [],
searchQuery: '',
));
AppLogger.i(_tag, '预设搜索已清除');
}
/// 刷新预设数据
Future<void> _onRefreshPresetData(
RefreshPresetData event,
Emitter<PresetState> emit,
) async {
// 重新加载所有数据
add(const LoadUserPresetOverview());
add(const LoadGroupedPresets());
AppLogger.i(_tag, '预设数据刷新中...');
}
/// 🚀 查找现有分组中相同功能类型的键(已统一格式,现在只做直接匹配)
String? _findExistingFeatureTypeKey(Map<String, List<AIPromptPreset>> groupedPresets, String newFeatureType) {
// 如果直接存在返回null使用新的键
if (groupedPresets.containsKey(newFeatureType)) {
return null;
}
// 🚀 已统一为新格式,不再需要映射,直接使用新的功能类型键
AppLogger.i(_tag, '📋 使用新的功能类型键: $newFeatureType');
return null;
}
/// 🚀 新增预设到本地缓存
Future<void> _onAddPresetToCache(
AddPresetToCache event,
Emitter<PresetState> emit,
) async {
try {
final newPreset = event.preset;
AppLogger.i(_tag, '🚀 添加新预设到本地缓存: ${newPreset.presetName}');
// 🚀 更新聚合数据缓存
if (state.allPresetData != null) {
final updatedData = _addPresetToAggregatedData(state.allPresetData!, newPreset);
// 同时更新分组预设以保持兼容性
final updatedGroupedPresets = Map<String, List<AIPromptPreset>>.from(state.groupedPresets);
final featureType = newPreset.aiFeatureType;
if (updatedGroupedPresets.containsKey(featureType)) {
// 将新预设添加到列表开头
updatedGroupedPresets[featureType] = [newPreset, ...updatedGroupedPresets[featureType]!];
} else {
// 创建新的功能类型分组
updatedGroupedPresets[featureType] = [newPreset];
}
emit(state.copyWith(
allPresetData: updatedData,
groupedPresets: updatedGroupedPresets,
errorMessage: null,
));
AppLogger.i(_tag, '✅ 预设已添加到本地缓存: ${featureType}');
} else {
// 如果没有聚合数据,只更新分组预设
final updatedGroupedPresets = Map<String, List<AIPromptPreset>>.from(state.groupedPresets);
final featureType = newPreset.aiFeatureType;
if (updatedGroupedPresets.containsKey(featureType)) {
updatedGroupedPresets[featureType] = [newPreset, ...updatedGroupedPresets[featureType]!];
} else {
updatedGroupedPresets[featureType] = [newPreset];
}
emit(state.copyWith(
groupedPresets: updatedGroupedPresets,
errorMessage: null,
));
AppLogger.w(_tag, '⚠️ 仅更新分组预设,聚合数据不存在');
}
} catch (e) {
AppLogger.e(_tag, '❌ 添加预设到本地缓存失败', e);
emit(state.copyWith(
errorMessage: '添加预设到缓存失败: ${e.toString()}',
));
}
}
/// 🚀 加载所有预设聚合数据
Future<void> _onLoadAllPresetData(
LoadAllPresetData event,
Emitter<PresetState> emit,
) async {
try {
emit(state.copyWith(isLoading: true, errorMessage: null));
AppLogger.i(_tag, '🚀 开始加载所有预设聚合数据: novelId=${event.novelId}');
final allPresetData = await _aggregationRepository.getAllUserPresetData(
novelId: event.novelId,
);
emit(state.copyWith(
isLoading: false,
allPresetData: allPresetData,
// 同时更新其他相关字段以保持兼容性
userOverview: allPresetData.overview,
groupedPresets: allPresetData.mergedGroupedPresets,
batchPackages: allPresetData.packagesByFeatureType,
favoritePresets: allPresetData.favoritePresets,
quickAccessPresets: allPresetData.quickAccessPresets,
recentlyUsedPresets: allPresetData.recentlyUsedPresets,
errorMessage: null,
));
AppLogger.i(_tag, '✅ 所有预设聚合数据加载完成');
AppLogger.i(_tag, '📊 数据统计: 系统预设${allPresetData.systemPresets.length}个, 用户预设分组${allPresetData.userPresetsByFeatureType.length}');
AppLogger.i(_tag, '📈 合并分组: ${allPresetData.mergedGroupedPresets.length}个功能类型');
allPresetData.mergedGroupedPresets.forEach((featureType, presets) {
AppLogger.i(_tag, ' - $featureType: ${presets.length}个预设');
});
} catch (e) {
AppLogger.e(_tag, '❌ 加载所有预设聚合数据失败', e);
emit(state.copyWith(
isLoading: false,
errorMessage: '加载预设数据失败: ${e.toString()}',
));
}
}
/// 预热预设缓存
Future<void> _onWarmupPresetCache(
WarmupPresetCache event,
Emitter<PresetState> emit,
) async {
try {
AppLogger.i(_tag, '开始预热预设缓存...');
final warmupResult = await _aggregationRepository.warmupCache();
emit(state.copyWith(
warmupResult: warmupResult,
errorMessage: null,
));
AppLogger.i(_tag, '预设缓存预热完成: ${warmupResult.success ? "成功" : "失败"}');
if (warmupResult.success) {
AppLogger.i(_tag, '预热了 ${warmupResult.warmedFeatureTypes} 个功能类型,${warmupResult.warmedPresets} 个预设,耗时 ${warmupResult.durationMs}ms');
}
} catch (e) {
AppLogger.e(_tag, '预设缓存预热失败', e);
emit(state.copyWith(
errorMessage: '预设缓存预热失败: ${e.toString()}',
));
}
}
/// 🚀 向聚合缓存中添加新预设
AllUserPresetData _addPresetToAggregatedData(AllUserPresetData data, AIPromptPreset newPreset) {
final featureType = newPreset.aiFeatureType;
// 更新用户预设分组
final userByFeature = Map<String, List<AIPromptPreset>>.from(data.userPresetsByFeatureType);
if (userByFeature.containsKey(featureType)) {
// 添加到现有分组的开头
userByFeature[featureType] = [newPreset, ...userByFeature[featureType]!];
} else {
// 创建新的功能类型分组
userByFeature[featureType] = [newPreset];
}
// 更新包分组(如果存在)
final packages = Map<String, PresetPackage>.from(data.packagesByFeatureType);
if (packages.containsKey(featureType)) {
final oldPackage = packages[featureType]!;
packages[featureType] = PresetPackage(
featureType: featureType,
systemPresets: oldPackage.systemPresets,
userPresets: [newPreset, ...oldPackage.userPresets],
favoritePresets: oldPackage.favoritePresets,
quickAccessPresets: oldPackage.quickAccessPresets,
recentlyUsedPresets: oldPackage.recentlyUsedPresets,
totalCount: oldPackage.totalCount + 1,
cachedAt: DateTime.now(),
);
}
// 如果新预设是收藏、快捷访问等特殊状态,也需要更新对应列表
final favoritePresets = newPreset.isFavorite
? [newPreset, ...data.favoritePresets]
: data.favoritePresets;
final quickAccessPresets = newPreset.showInQuickAccess
? [newPreset, ...data.quickAccessPresets]
: data.quickAccessPresets;
// 添加到最近使用列表的开头
final recentlyUsedPresets = [newPreset, ...data.recentlyUsedPresets];
// 更新概览统计
final currentStats = data.overview.presetsByFeatureType[featureType];
final updatedStats = currentStats != null
? PresetTypeStats(
systemCount: currentStats.systemCount,
userCount: currentStats.userCount + 1,
favoriteCount: newPreset.isFavorite ? currentStats.favoriteCount + 1 : currentStats.favoriteCount,
recentUsageCount: currentStats.recentUsageCount + 1,
)
: PresetTypeStats(
systemCount: 0,
userCount: 1,
favoriteCount: newPreset.isFavorite ? 1 : 0,
recentUsageCount: 1,
);
final overview = UserPresetOverview(
totalPresets: data.overview.totalPresets + 1,
systemPresets: data.overview.systemPresets,
userPresets: data.overview.userPresets + 1,
favoritePresets: favoritePresets.length,
presetsByFeatureType: {
...data.overview.presetsByFeatureType,
featureType: updatedStats,
},
recentFeatureTypes: _updateRecentFeatureTypes(data.overview.recentFeatureTypes, featureType),
popularTags: data.overview.popularTags,
generatedAt: DateTime.now(),
);
return AllUserPresetData(
userId: data.userId,
overview: overview,
packagesByFeatureType: packages,
systemPresets: data.systemPresets,
userPresetsByFeatureType: userByFeature,
favoritePresets: favoritePresets,
quickAccessPresets: quickAccessPresets,
recentlyUsedPresets: recentlyUsedPresets,
timestamp: DateTime.now(),
cacheDuration: data.cacheDuration,
);
}
/// 🚀 更新最近使用的功能类型列表
List<String> _updateRecentFeatureTypes(List<String> current, String newFeatureType) {
final updated = [newFeatureType];
for (final type in current) {
if (type != newFeatureType && updated.length < 5) {
updated.add(type);
}
}
return updated;
}
/// 🚀 从聚合缓存中删除指定预设
AllUserPresetData? _removePresetFromAggregatedData(AllUserPresetData? data, String presetId) {
if (data == null) return null;
bool found = false;
// 从系统预设列表中移除
final system = data.systemPresets.where((p) => p.presetId != presetId).toList();
if (system.length != data.systemPresets.length) found = true;
// 从用户预设分组中移除
final userByFeature = <String, List<AIPromptPreset>>{};
data.userPresetsByFeatureType.forEach((k, list) {
final filtered = list.where((p) => p.presetId != presetId).toList();
if (filtered.isNotEmpty) {
userByFeature[k] = filtered;
}
if (filtered.length != list.length) found = true;
});
// 从收藏/快捷/最近列表中移除
final fav = data.favoritePresets.where((p) => p.presetId != presetId).toList();
final quick = data.quickAccessPresets.where((p) => p.presetId != presetId).toList();
final recent = data.recentlyUsedPresets.where((p) => p.presetId != presetId).toList();
if (!found) return data; // 未找到则直接返回原数据
// 更新包分组
final packages = Map<String, PresetPackage>.from(data.packagesByFeatureType);
packages.forEach((featureType, package) {
final filteredUser = package.userPresets.where((p) => p.presetId != presetId).toList();
final filteredSystem = package.systemPresets.where((p) => p.presetId != presetId).toList();
if (filteredUser.length != package.userPresets.length ||
filteredSystem.length != package.systemPresets.length) {
packages[featureType] = PresetPackage(
featureType: featureType,
systemPresets: filteredSystem,
userPresets: filteredUser,
favoritePresets: package.favoritePresets.where((p) => p.presetId != presetId).toList(),
quickAccessPresets: package.quickAccessPresets.where((p) => p.presetId != presetId).toList(),
recentlyUsedPresets: package.recentlyUsedPresets.where((p) => p.presetId != presetId).toList(),
totalCount: filteredUser.length + filteredSystem.length,
cachedAt: DateTime.now(),
);
}
});
// 更新概览统计
final overview = UserPresetOverview(
totalPresets: data.overview.totalPresets - 1,
systemPresets: system.length,
userPresets: userByFeature.values.fold(0, (sum, list) => sum + list.length),
favoritePresets: fav.length,
presetsByFeatureType: data.overview.presetsByFeatureType, // 保持不变,可选优化
recentFeatureTypes: data.overview.recentFeatureTypes,
popularTags: data.overview.popularTags,
generatedAt: DateTime.now(),
);
return AllUserPresetData(
userId: data.userId,
overview: overview,
packagesByFeatureType: packages,
systemPresets: system,
userPresetsByFeatureType: userByFeature,
favoritePresets: fav,
quickAccessPresets: quick,
recentlyUsedPresets: recent,
timestamp: DateTime.now(),
cacheDuration: data.cacheDuration,
);
}
/// 🚀 在聚合缓存中替换指定预设
AllUserPresetData? _replacePresetInAggregatedData(AllUserPresetData? data, AIPromptPreset updated) {
if (data == null) return null;
bool replaced = false;
// 更新系统预设列表
List<AIPromptPreset> system = data.systemPresets
.map((p) => p.presetId == updated.presetId ? updated : p)
.toList();
if (!replaced) replaced = system.any((p) => p.presetId == updated.presetId);
// 更新用户预设分组
final userByFeature = <String, List<AIPromptPreset>>{};
data.userPresetsByFeatureType.forEach((k, list) {
userByFeature[k] = list.map((p) => p.presetId == updated.presetId ? updated : p).toList();
if (!replaced) {
replaced = list.any((p) => p.presetId == updated.presetId);
}
});
// 更新收藏/快捷/最近
List<AIPromptPreset> _mapList(List<AIPromptPreset> src) =>
src.map((p) => p.presetId == updated.presetId ? updated : p).toList();
final fav = _mapList(data.favoritePresets);
final quick = _mapList(data.quickAccessPresets);
final recent = _mapList(data.recentlyUsedPresets);
// 如果所有列表都未包含,则根据预设类型追加到正确列表
if (!replaced) {
if (updated.isSystem) {
system.add(updated);
} else {
userByFeature.putIfAbsent(updated.aiFeatureType, () => []);
userByFeature[updated.aiFeatureType]!.add(updated);
}
// 快捷访问
if (updated.showInQuickAccess && !quick.any((p) => p.presetId == updated.presetId)) {
quick.insert(0, updated);
}
// 收藏
if (updated.isFavorite && !fav.any((p) => p.presetId == updated.presetId)) {
fav.insert(0, updated);
}
// 最近使用无需处理
}
return AllUserPresetData(
userId: data.userId,
overview: data.overview,
packagesByFeatureType: data.packagesByFeatureType,
systemPresets: system,
userPresetsByFeatureType: userByFeature,
favoritePresets: fav,
quickAccessPresets: quick,
recentlyUsedPresets: recent,
timestamp: DateTime.now(), // 🔧 修复:更新为当前时间戳
cacheDuration: data.cacheDuration,
);
}
}

View File

@@ -0,0 +1,272 @@
import 'package:equatable/equatable.dart';
import 'package:ainoval/models/preset_models.dart';
/// 预设管理事件基类
abstract class PresetEvent extends Equatable {
const PresetEvent();
@override
List<Object?> get props => [];
}
/// 加载用户预设概览
class LoadUserPresetOverview extends PresetEvent {
const LoadUserPresetOverview();
}
/// 加载预设包
class LoadPresetPackage extends PresetEvent {
final String featureType;
final String? novelId;
const LoadPresetPackage({
required this.featureType,
this.novelId,
});
@override
List<Object?> get props => [featureType, novelId];
}
/// 加载批量预设包
class LoadBatchPresetPackages extends PresetEvent {
final List<String>? featureTypes;
final String? novelId;
const LoadBatchPresetPackages({
this.featureTypes,
this.novelId,
});
@override
List<Object?> get props => [featureTypes, novelId];
}
/// 加载分组预设
class LoadGroupedPresets extends PresetEvent {
final String? userId;
const LoadGroupedPresets({this.userId});
@override
List<Object?> get props => [userId];
}
/// 选择预设
class SelectPreset extends PresetEvent {
final String presetId;
const SelectPreset({required this.presetId});
@override
List<Object?> get props => [presetId];
}
/// 创建预设
class CreatePreset extends PresetEvent {
final CreatePresetRequest request;
const CreatePreset({required this.request});
@override
List<Object?> get props => [request];
}
/// 覆盖更新预设(完整对象)
class OverwritePreset extends PresetEvent {
final AIPromptPreset preset;
const OverwritePreset({required this.preset});
@override
List<Object?> get props => [preset];
}
/// 更新预设
class UpdatePreset extends PresetEvent {
final String presetId;
final UpdatePresetInfoRequest? infoRequest;
final UpdatePresetPromptsRequest? promptsRequest;
const UpdatePreset({
required this.presetId,
this.infoRequest,
this.promptsRequest,
});
@override
List<Object?> get props => [presetId, infoRequest, promptsRequest];
}
/// 删除预设
class DeletePreset extends PresetEvent {
final String presetId;
const DeletePreset({required this.presetId});
@override
List<Object?> get props => [presetId];
}
/// 复制预设
class DuplicatePreset extends PresetEvent {
final String presetId;
final DuplicatePresetRequest request;
const DuplicatePreset({
required this.presetId,
required this.request,
});
@override
List<Object?> get props => [presetId, request];
}
/// 切换预设收藏状态
class TogglePresetFavorite extends PresetEvent {
final String presetId;
const TogglePresetFavorite({required this.presetId});
@override
List<Object?> get props => [presetId];
}
/// 切换预设快捷访问状态
class TogglePresetQuickAccess extends PresetEvent {
final String presetId;
const TogglePresetQuickAccess({required this.presetId});
@override
List<Object?> get props => [presetId];
}
/// 记录预设使用
class RecordPresetUsage extends PresetEvent {
final String presetId;
const RecordPresetUsage({required this.presetId});
@override
List<Object?> get props => [presetId];
}
/// 搜索预设
class SearchPresets extends PresetEvent {
final String query;
final String? featureType;
final List<String>? tags;
final String? sortBy;
const SearchPresets({
required this.query,
this.featureType,
this.tags,
this.sortBy,
});
@override
List<Object?> get props => [query, featureType, tags, sortBy];
}
/// 清除预设搜索
class ClearPresetSearch extends PresetEvent {
const ClearPresetSearch();
}
/// 获取预设统计信息
class LoadPresetStatistics extends PresetEvent {
const LoadPresetStatistics();
}
/// 获取收藏预设
class LoadFavoritePresets extends PresetEvent {
final String? novelId;
final String? featureType;
const LoadFavoritePresets({
this.novelId,
this.featureType,
});
@override
List<Object?> get props => [novelId, featureType];
}
/// 获取最近使用预设
class LoadRecentlyUsedPresets extends PresetEvent {
final int limit;
final String? novelId;
final String? featureType;
const LoadRecentlyUsedPresets({
this.limit = 10,
this.novelId,
this.featureType,
});
@override
List<Object?> get props => [limit, novelId, featureType];
}
/// 获取快捷访问预设
class LoadQuickAccessPresets extends PresetEvent {
final String? featureType;
final String? novelId;
const LoadQuickAccessPresets({
this.featureType,
this.novelId,
});
@override
List<Object?> get props => [featureType, novelId];
}
/// 刷新预设数据
class RefreshPresetData extends PresetEvent {
const RefreshPresetData();
}
/// 预热缓存
class WarmupPresetCache extends PresetEvent {
const WarmupPresetCache();
}
/// 获取缓存统计
class LoadCacheStats extends PresetEvent {
const LoadCacheStats();
}
/// 清除缓存
class ClearPresetCache extends PresetEvent {
const ClearPresetCache();
}
/// 健康检查
class PresetHealthCheck extends PresetEvent {
const PresetHealthCheck();
}
/// 🚀 加载所有预设聚合数据
/// 一次性加载用户的所有预设相关数据避免多次API调用
class LoadAllPresetData extends PresetEvent {
final String? novelId;
const LoadAllPresetData({this.novelId});
@override
List<Object?> get props => [novelId];
}
/// 🚀 新增预设到本地缓存
/// 创建预设成功后直接添加到本地缓存,避免重新加载
class AddPresetToCache extends PresetEvent {
final AIPromptPreset preset;
const AddPresetToCache({required this.preset});
@override
List<Object?> get props => [preset];
}

View File

@@ -0,0 +1,240 @@
import 'package:equatable/equatable.dart';
import 'package:ainoval/models/preset_models.dart';
/// 预设管理状态
class PresetState extends Equatable {
/// 是否正在加载
final bool isLoading;
/// 错误信息
final String? errorMessage;
/// 用户预设概览
final UserPresetOverview? userOverview;
/// 当前预设包
final PresetPackage? currentPackage;
/// 批量预设包
final Map<String, PresetPackage> batchPackages;
/// 按功能类型分组的预设
final Map<String, List<AIPromptPreset>> groupedPresets;
/// 当前选中的预设
final AIPromptPreset? selectedPreset;
/// 搜索结果
final List<AIPromptPreset> searchResults;
/// 搜索查询
final String searchQuery;
/// 预设统计信息
final PresetStatistics? statistics;
/// 收藏预设列表
final List<AIPromptPreset> favoritePresets;
/// 最近使用预设列表
final List<AIPromptPreset> recentlyUsedPresets;
/// 快捷访问预设列表
final List<AIPromptPreset> quickAccessPresets;
/// 缓存预热结果
final CacheWarmupResult? warmupResult;
/// 缓存统计信息
final AggregationCacheStats? cacheStats;
/// 健康检查结果
final Map<String, dynamic>? healthStatus;
/// 🚀 所有预设聚合数据
final AllUserPresetData? allPresetData;
const PresetState({
this.isLoading = false,
this.errorMessage,
this.userOverview,
this.currentPackage,
this.batchPackages = const {},
this.groupedPresets = const {},
this.selectedPreset,
this.searchResults = const [],
this.searchQuery = '',
this.statistics,
this.favoritePresets = const [],
this.recentlyUsedPresets = const [],
this.quickAccessPresets = const [],
this.warmupResult,
this.cacheStats,
this.healthStatus,
this.allPresetData,
});
/// 初始状态
const PresetState.initial() : this();
/// 加载状态
PresetState.loading() : this(isLoading: true);
/// 错误状态
PresetState.error(String message) : this(errorMessage: message);
/// 复制状态并更新指定字段
PresetState copyWith({
bool? isLoading,
String? errorMessage,
UserPresetOverview? userOverview,
PresetPackage? currentPackage,
Map<String, PresetPackage>? batchPackages,
Map<String, List<AIPromptPreset>>? groupedPresets,
AIPromptPreset? selectedPreset,
List<AIPromptPreset>? searchResults,
String? searchQuery,
PresetStatistics? statistics,
List<AIPromptPreset>? favoritePresets,
List<AIPromptPreset>? recentlyUsedPresets,
List<AIPromptPreset>? quickAccessPresets,
CacheWarmupResult? warmupResult,
AggregationCacheStats? cacheStats,
Map<String, dynamic>? healthStatus,
AllUserPresetData? allPresetData,
}) {
return PresetState(
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage,
userOverview: userOverview ?? this.userOverview,
currentPackage: currentPackage ?? this.currentPackage,
batchPackages: batchPackages ?? this.batchPackages,
groupedPresets: groupedPresets ?? this.groupedPresets,
selectedPreset: selectedPreset,
searchResults: searchResults ?? this.searchResults,
searchQuery: searchQuery ?? this.searchQuery,
statistics: statistics ?? this.statistics,
favoritePresets: favoritePresets ?? this.favoritePresets,
recentlyUsedPresets: recentlyUsedPresets ?? this.recentlyUsedPresets,
quickAccessPresets: quickAccessPresets ?? this.quickAccessPresets,
warmupResult: warmupResult ?? this.warmupResult,
cacheStats: cacheStats ?? this.cacheStats,
healthStatus: healthStatus ?? this.healthStatus,
allPresetData: allPresetData ?? this.allPresetData,
);
}
/// 是否有数据
bool get hasData {
return userOverview != null ||
currentPackage != null ||
batchPackages.isNotEmpty ||
groupedPresets.isNotEmpty ||
searchResults.isNotEmpty;
}
/// 是否有错误
bool get hasError => errorMessage != null;
/// 是否有选中的预设
bool get hasSelectedPreset => selectedPreset != null;
/// 是否正在搜索
bool get isSearching => searchQuery.isNotEmpty;
/// 获取所有预设的总数
int get totalPresetCount {
return groupedPresets.values.fold(0, (sum, presets) => sum + presets.length);
}
/// 获取用户预设数量
int get userPresetCount {
return groupedPresets.values
.expand((presets) => presets)
.where((preset) => !preset.isSystem)
.length;
}
/// 获取系统预设数量
int get systemPresetCount {
return groupedPresets.values
.expand((presets) => presets)
.where((preset) => preset.isSystem)
.length;
}
/// 获取收藏预设数量
int get favoritePresetCount {
return groupedPresets.values
.expand((presets) => presets)
.where((preset) => preset.isFavorite)
.length;
}
/// 获取快捷访问预设数量
int get quickAccessPresetCount {
return groupedPresets.values
.expand((presets) => presets)
.where((preset) => preset.showInQuickAccess)
.length;
}
/// 获取指定功能类型的预设列表
List<AIPromptPreset> getPresetsByFeatureType(String featureType) {
return groupedPresets[featureType] ?? [];
}
/// 获取所有预设的平铺列表
List<AIPromptPreset> get allPresets {
return groupedPresets.values.expand((presets) => presets).toList();
}
/// 🚀 获取合并后的分组预设(系统预设+用户预设,按功能分组)
/// 优先使用allPresetData中的合并数据如果没有则使用旧的groupedPresets
Map<String, List<AIPromptPreset>> get mergedGroupedPresets {
if (allPresetData != null) {
return allPresetData!.mergedGroupedPresets;
}
return groupedPresets;
}
/// 是否已加载聚合数据
bool get hasAllPresetData => allPresetData != null;
@override
List<Object?> get props => [
isLoading,
errorMessage,
userOverview,
currentPackage,
batchPackages,
groupedPresets,
selectedPreset,
searchResults,
searchQuery,
statistics,
favoritePresets,
recentlyUsedPresets,
quickAccessPresets,
warmupResult,
cacheStats,
healthStatus,
allPresetData,
];
@override
String toString() {
return '''PresetState(
isLoading: $isLoading,
hasError: $hasError,
hasData: $hasData,
totalPresets: $totalPresetCount,
userPresets: $userPresetCount,
systemPresets: $systemPresetCount,
favoritePresets: $favoritePresetCount,
quickAccessPresets: $quickAccessPresetCount,
selectedPreset: ${selectedPreset?.presetName ?? 'null'},
searchQuery: '$searchQuery',
)''';
}
}

View File

@@ -0,0 +1,632 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ainoval/models/prompt_models.dart';
import 'package:ainoval/services/api_service/repositories/prompt_repository.dart';
import 'package:ainoval/utils/logger.dart';
import 'prompt_new_event.dart';
import 'prompt_new_state.dart';
/// 提示词管理BLoC
class PromptNewBloc extends Bloc<PromptNewEvent, PromptNewState> {
PromptNewBloc({
required PromptRepository promptRepository,
}) : _promptRepository = promptRepository,
super(const PromptNewState()) {
on<LoadAllPromptPackages>(_onLoadAllPromptPackages);
on<SelectPrompt>(_onSelectPrompt);
on<CreateNewPrompt>(_onCreateNewPrompt);
on<UpdatePromptDetails>(_onUpdatePromptDetails);
on<CopyPromptTemplate>(_onCopyPromptTemplate);
on<ToggleFavoriteStatus>(_onToggleFavoriteStatus);
on<SetDefaultTemplate>(_onSetDefaultTemplate);
on<DeletePrompt>(_onDeletePrompt);
on<SearchPrompts>(_onSearchPrompts);
on<ClearSearch>(_onClearSearch);
on<ToggleViewMode>(_onToggleViewMode);
on<RefreshPromptData>(_onRefreshPromptData);
}
final PromptRepository _promptRepository;
static const String _tag = 'PromptNewBloc';
/// 将EnhancedUserPromptTemplate转换为UserPromptInfo的辅助函数
UserPromptInfo _convertToUserPromptInfo(EnhancedUserPromptTemplate template) {
return UserPromptInfo(
id: template.id,
name: template.name,
description: template.description,
featureType: template.featureType,
systemPrompt: template.systemPrompt,
userPrompt: template.userPrompt,
tags: template.tags,
categories: template.categories,
isFavorite: template.isFavorite,
isDefault: template.isDefault,
isPublic: template.isPublic,
shareCode: template.shareCode,
usageCount: template.usageCount,
rating: template.rating,
authorId: template.userId, // 使用userId作为authorId
createdAt: template.createdAt,
lastUsedAt: template.lastUsedAt,
updatedAt: template.updatedAt,
);
}
/// 加载所有提示词包
Future<void> _onLoadAllPromptPackages(
LoadAllPromptPackages event,
Emitter<PromptNewState> emit,
) async {
try {
emit(state.copyWith(status: PromptNewStatus.loading));
AppLogger.i(_tag, '开始加载所有提示词包');
// 使用批量获取API
final promptPackages = await _promptRepository.getBatchPromptPackages(
includePublic: true,
);
AppLogger.i(_tag, '成功加载提示词包,功能类型数量: ${promptPackages.length}');
emit(state.copyWith(
status: PromptNewStatus.success,
promptPackages: promptPackages,
errorMessage: null,
));
} catch (error) {
AppLogger.e(_tag, '加载提示词包失败', error);
emit(state.copyWith(
status: PromptNewStatus.failure,
errorMessage: '加载提示词包失败: ${error.toString()}',
));
}
}
/// 选择提示词
Future<void> _onSelectPrompt(
SelectPrompt event,
Emitter<PromptNewState> emit,
) async {
AppLogger.i(_tag, '选择提示词: ${event.promptId}, 功能类型: ${event.featureType}');
emit(state.copyWith(
selectedPromptId: event.promptId,
selectedFeatureType: event.featureType,
viewMode: PromptViewMode.detail,
));
}
/// 创建新提示词
Future<void> _onCreateNewPrompt(
CreateNewPrompt event,
Emitter<PromptNewState> emit,
) async {
try {
emit(state.copyWith(isCreating: true));
AppLogger.i(_tag, '开始创建新提示词,功能类型: ${event.featureType}');
// 创建新提示词模板
final request = CreatePromptTemplateRequest(
name: '新提示词模板 ${DateTime.now().millisecondsSinceEpoch}',
description: '用户创建的提示词模板',
featureType: event.featureType,
systemPrompt: '',
userPrompt: '',
tags: [],
categories: [],
);
final newTemplate = await _promptRepository.createEnhancedPromptTemplate(request);
AppLogger.i(_tag, '成功创建新提示词模板: ${newTemplate.id}');
// 直接在本地状态添加新模板,无需重新请求所有数据
final updatedPackages = Map<AIFeatureType, PromptPackage>.from(state.promptPackages);
final package = updatedPackages[event.featureType];
if (package != null) {
// 将EnhancedUserPromptTemplate转换为UserPromptInfo
final newUserPrompt = _convertToUserPromptInfo(newTemplate);
// 创建新的用户提示词列表
final updatedUserPrompts = List<UserPromptInfo>.from(package.userPrompts);
updatedUserPrompts.add(newUserPrompt);
// 更新package
updatedPackages[event.featureType] = PromptPackage(
featureType: package.featureType,
systemPrompt: package.systemPrompt,
userPrompts: updatedUserPrompts,
publicPrompts: package.publicPrompts,
recentlyUsed: package.recentlyUsed,
supportedPlaceholders: package.supportedPlaceholders,
placeholderDescriptions: package.placeholderDescriptions,
lastUpdated: DateTime.now(),
);
// 发出新状态,选择新创建的提示词
emit(state.copyWith(
isCreating: false,
promptPackages: updatedPackages,
selectedPromptId: newTemplate.id,
selectedFeatureType: event.featureType,
viewMode: PromptViewMode.detail,
errorMessage: null,
));
AppLogger.i(_tag, '本地状态已更新,新模板已添加到列表并选中');
} else {
AppLogger.w(_tag, '无法找到功能类型包: ${event.featureType}');
emit(state.copyWith(isCreating: false));
}
} catch (error) {
AppLogger.e(_tag, '创建新提示词失败', error);
emit(state.copyWith(
isCreating: false,
errorMessage: '创建新提示词失败: ${error.toString()}',
));
}
}
/// 更新提示词详情
Future<void> _onUpdatePromptDetails(
UpdatePromptDetails event,
Emitter<PromptNewState> emit,
) async {
try {
emit(state.copyWith(isUpdating: true));
AppLogger.i(_tag, '开始更新提示词详情: ${event.promptId}');
final updatedTemplate = await _promptRepository.updateEnhancedPromptTemplate(
event.promptId,
event.request,
);
AppLogger.i(_tag, '成功更新提示词详情: ${event.promptId}');
// 直接在本地状态更新提示词详情,无需重新请求所有数据
final updatedPackages = Map<AIFeatureType, PromptPackage>.from(state.promptPackages);
bool updated = false;
for (final entry in updatedPackages.entries) {
final package = entry.value;
final updatedUserPrompts = package.userPrompts.map((prompt) {
if (prompt.id == event.promptId) {
updated = true;
return _convertToUserPromptInfo(updatedTemplate);
}
return prompt;
}).toList();
if (updated) {
updatedPackages[entry.key] = PromptPackage(
featureType: package.featureType,
systemPrompt: package.systemPrompt,
userPrompts: updatedUserPrompts,
publicPrompts: package.publicPrompts,
recentlyUsed: package.recentlyUsed,
supportedPlaceholders: package.supportedPlaceholders,
placeholderDescriptions: package.placeholderDescriptions,
lastUpdated: DateTime.now(),
);
break;
}
}
if (updated) {
emit(state.copyWith(
isUpdating: false,
promptPackages: updatedPackages,
errorMessage: null,
));
AppLogger.i(_tag, '本地状态已更新,提示词详情已更新');
} else {
AppLogger.w(_tag, '未找到需要更新的提示词: ${event.promptId}');
emit(state.copyWith(isUpdating: false));
}
} catch (error) {
AppLogger.e(_tag, '更新提示词详情失败', error);
emit(state.copyWith(
isUpdating: false,
errorMessage: '更新提示词详情失败: ${error.toString()}',
));
}
}
/// 复制提示词模板
Future<void> _onCopyPromptTemplate(
CopyPromptTemplate event,
Emitter<PromptNewState> emit,
) async {
try {
AppLogger.i(_tag, '开始复制提示词模板: ${event.templateId}');
final copiedTemplate = await _promptRepository.copyPublicEnhancedTemplate(
event.templateId,
);
AppLogger.i(_tag, '成功复制提示词模板: ${copiedTemplate.id}');
// 直接在本地状态添加新模板,无需重新请求所有数据
final updatedPackages = Map<AIFeatureType, PromptPackage>.from(state.promptPackages);
final package = updatedPackages[copiedTemplate.featureType];
if (package != null) {
// 将EnhancedUserPromptTemplate转换为UserPromptInfo
final newUserPrompt = _convertToUserPromptInfo(copiedTemplate);
// 创建新的用户提示词列表
final updatedUserPrompts = List<UserPromptInfo>.from(package.userPrompts);
updatedUserPrompts.add(newUserPrompt);
// 更新package
updatedPackages[copiedTemplate.featureType] = PromptPackage(
featureType: package.featureType,
systemPrompt: package.systemPrompt,
userPrompts: updatedUserPrompts,
publicPrompts: package.publicPrompts,
recentlyUsed: package.recentlyUsed,
supportedPlaceholders: package.supportedPlaceholders,
placeholderDescriptions: package.placeholderDescriptions,
lastUpdated: DateTime.now(),
);
// 发出新状态
emit(state.copyWith(
promptPackages: updatedPackages,
selectedPromptId: copiedTemplate.id,
selectedFeatureType: copiedTemplate.featureType,
errorMessage: null,
));
AppLogger.i(_tag, '本地状态已更新,新模板已添加到列表');
} else {
AppLogger.w(_tag, '无法找到功能类型包: ${copiedTemplate.featureType}');
// 如果找不到对应的包则fallback到刷新数据
add(const RefreshPromptData());
add(SelectPrompt(
promptId: copiedTemplate.id,
featureType: copiedTemplate.featureType,
));
}
} catch (error) {
AppLogger.e(_tag, '复制提示词模板失败', error);
emit(state.copyWith(
errorMessage: '复制提示词模板失败: ${error.toString()}',
));
}
}
/// 切换收藏状态
Future<void> _onToggleFavoriteStatus(
ToggleFavoriteStatus event,
Emitter<PromptNewState> emit,
) async {
try {
AppLogger.i(_tag, '切换收藏状态: ${event.promptId}, 收藏: ${event.isFavorite}');
if (event.isFavorite) {
await _promptRepository.favoriteEnhancedTemplate(event.promptId);
} else {
await _promptRepository.unfavoriteEnhancedTemplate(event.promptId);
}
// 直接在本地状态更新收藏状态,无需重新请求所有数据
final updatedPackages = Map<AIFeatureType, PromptPackage>.from(state.promptPackages);
bool updated = false;
for (final entry in updatedPackages.entries) {
final package = entry.value;
final updatedUserPrompts = package.userPrompts.map((prompt) {
if (prompt.id == event.promptId) {
updated = true;
return prompt.copyWith(
isFavorite: event.isFavorite,
updatedAt: DateTime.now(),
);
}
return prompt;
}).toList();
if (updated) {
updatedPackages[entry.key] = PromptPackage(
featureType: package.featureType,
systemPrompt: package.systemPrompt,
userPrompts: updatedUserPrompts,
publicPrompts: package.publicPrompts,
recentlyUsed: package.recentlyUsed,
supportedPlaceholders: package.supportedPlaceholders,
placeholderDescriptions: package.placeholderDescriptions,
lastUpdated: DateTime.now(),
);
break;
}
}
if (updated) {
emit(state.copyWith(
promptPackages: updatedPackages,
errorMessage: null,
));
AppLogger.i(_tag, '本地状态已更新,收藏状态已切换');
} else {
AppLogger.w(_tag, '未找到需要更新的提示词: ${event.promptId}');
// 如果找不到对应的提示词则fallback到刷新数据
add(const RefreshPromptData());
}
} catch (error) {
AppLogger.e(_tag, '切换收藏状态失败', error);
emit(state.copyWith(
errorMessage: '切换收藏状态失败: ${error.toString()}',
));
}
}
/// 删除提示词
Future<void> _onDeletePrompt(
DeletePrompt event,
Emitter<PromptNewState> emit,
) async {
try {
AppLogger.i(_tag, '开始删除提示词: ${event.promptId}');
await _promptRepository.deleteEnhancedPromptTemplate(event.promptId);
AppLogger.i(_tag, '成功删除提示词: ${event.promptId}');
// 直接在本地状态删除提示词,无需重新请求所有数据
final updatedPackages = Map<AIFeatureType, PromptPackage>.from(state.promptPackages);
bool deleted = false;
for (final entry in updatedPackages.entries) {
final package = entry.value;
final originalLength = package.userPrompts.length;
final updatedUserPrompts = package.userPrompts
.where((prompt) => prompt.id != event.promptId)
.toList();
if (updatedUserPrompts.length < originalLength) {
deleted = true;
updatedPackages[entry.key] = PromptPackage(
featureType: package.featureType,
systemPrompt: package.systemPrompt,
userPrompts: updatedUserPrompts,
publicPrompts: package.publicPrompts,
recentlyUsed: package.recentlyUsed,
supportedPlaceholders: package.supportedPlaceholders,
placeholderDescriptions: package.placeholderDescriptions,
lastUpdated: DateTime.now(),
);
break;
}
}
// 更新状态
final newState = state.copyWith(
promptPackages: updatedPackages,
errorMessage: null,
);
// 如果删除的是当前选中的提示词,清除选择
final finalState = state.selectedPromptId == event.promptId
? newState.clearSelection()
: newState;
emit(finalState);
if (deleted) {
AppLogger.i(_tag, '本地状态已更新,提示词已从列表中删除');
} else {
AppLogger.w(_tag, '未找到需要删除的提示词: ${event.promptId}');
}
} catch (error) {
AppLogger.e(_tag, '删除提示词失败', error);
emit(state.copyWith(
errorMessage: '删除提示词失败: ${error.toString()}',
));
}
}
/// 搜索提示词
Future<void> _onSearchPrompts(
SearchPrompts event,
Emitter<PromptNewState> emit,
) async {
AppLogger.i(_tag, '搜索提示词: ${event.query}');
final filteredPrompts = <AIFeatureType, List<UserPromptInfo>>{};
if (event.query.isEmpty) {
// 如果搜索查询为空清空过滤结果让UI使用正常的分组逻辑
emit(state.copyWith(
searchQuery: '',
filteredPrompts: {},
));
return;
}
// 过滤提示词
final query = event.query.toLowerCase();
for (final entry in state.promptPackages.entries) {
final featureType = entry.key;
final package = entry.value;
final allPrompts = <UserPromptInfo>[];
// 1. 添加系统默认提示词
if (package.systemPrompt.defaultSystemPrompt.isNotEmpty) {
final systemPromptAsUser = UserPromptInfo(
id: 'system_default_${featureType.toString()}',
name: '系统默认模板',
description: '系统提供的默认提示词模板',
featureType: featureType,
systemPrompt: package.systemPrompt.effectivePrompt,
userPrompt: package.systemPrompt.defaultUserPrompt,
tags: const ['系统默认'],
authorId: 'system',
createdAt: package.lastUpdated,
updatedAt: package.lastUpdated,
);
allPrompts.add(systemPromptAsUser);
}
// 2. 添加用户自定义提示词
allPrompts.addAll(package.userPrompts);
// 3. 添加公开提示词
for (final publicPrompt in package.publicPrompts) {
final publicPromptAsUser = UserPromptInfo(
id: 'public_${publicPrompt.id}',
name: '${publicPrompt.name} ${publicPrompt.isVerified ? '' : ''}',
description: '${publicPrompt.description ?? ''} (作者: ${publicPrompt.authorName ?? '匿名'})',
featureType: featureType,
systemPrompt: publicPrompt.systemPrompt,
userPrompt: publicPrompt.userPrompt,
tags: const ['公开模板'],
categories: publicPrompt.categories,
isPublic: true,
shareCode: publicPrompt.shareCode,
isVerified: publicPrompt.isVerified,
usageCount: publicPrompt.usageCount.toInt(),
favoriteCount: publicPrompt.favoriteCount.toInt(),
rating: publicPrompt.rating ?? 0.0,
authorId: publicPrompt.authorName,
version: publicPrompt.version,
language: publicPrompt.language,
createdAt: publicPrompt.createdAt,
lastUsedAt: publicPrompt.lastUsedAt,
updatedAt: publicPrompt.updatedAt,
);
allPrompts.add(publicPromptAsUser);
}
// 过滤匹配的提示词
final filtered = allPrompts.where((prompt) {
return prompt.name.toLowerCase().contains(query) ||
prompt.description?.toLowerCase().contains(query) == true ||
prompt.tags.any((tag) => tag.toLowerCase().contains(query));
}).toList();
if (filtered.isNotEmpty) {
filteredPrompts[featureType] = filtered;
}
}
emit(state.copyWith(
searchQuery: event.query,
filteredPrompts: filteredPrompts,
));
}
/// 清除搜索
Future<void> _onClearSearch(
ClearSearch event,
Emitter<PromptNewState> emit,
) async {
AppLogger.i(_tag, '清除搜索');
emit(state.copyWith(
searchQuery: '',
filteredPrompts: {},
));
}
/// 切换视图模式
Future<void> _onToggleViewMode(
ToggleViewMode event,
Emitter<PromptNewState> emit,
) async {
final newMode = state.viewMode == PromptViewMode.list
? PromptViewMode.detail
: PromptViewMode.list;
AppLogger.i(_tag, '切换视图模式: ${state.viewMode} -> $newMode');
emit(state.copyWith(viewMode: newMode));
}
/// 刷新提示词数据
Future<void> _onRefreshPromptData(
RefreshPromptData event,
Emitter<PromptNewState> emit,
) async {
// 重新加载数据,但不显示加载状态
try {
AppLogger.i(_tag, '刷新提示词数据');
final promptPackages = await _promptRepository.getBatchPromptPackages(
includePublic: true,
);
emit(state.copyWith(
promptPackages: promptPackages,
errorMessage: null,
));
AppLogger.i(_tag, '提示词数据刷新完成');
} catch (error) {
AppLogger.e(_tag, '刷新提示词数据失败', error);
emit(state.copyWith(
errorMessage: '刷新数据失败: ${error.toString()}',
));
}
}
/// 设置默认模板
Future<void> _onSetDefaultTemplate(
SetDefaultTemplate event,
Emitter<PromptNewState> emit,
) async {
try {
AppLogger.i(_tag, '设置默认模板: ${event.promptId}, 功能类型: ${event.featureType}');
await _promptRepository.setDefaultEnhancedTemplate(event.promptId);
AppLogger.i(_tag, '成功设置默认模板: ${event.promptId}');
// 直接在本地状态更新默认状态,无需重新请求所有数据
final updatedPackages = Map<AIFeatureType, PromptPackage>.from(state.promptPackages);
bool updated = false;
final package = updatedPackages[event.featureType];
if (package != null) {
// 先清除该功能类型下所有模板的默认状态
final updatedUserPrompts = package.userPrompts.map((prompt) {
return prompt.copyWith(
isDefault: prompt.id == event.promptId, // 只有目标模板设为默认
);
}).toList();
updated = true;
updatedPackages[event.featureType] = PromptPackage(
featureType: package.featureType,
systemPrompt: package.systemPrompt,
userPrompts: updatedUserPrompts,
publicPrompts: package.publicPrompts,
recentlyUsed: package.recentlyUsed,
supportedPlaceholders: package.supportedPlaceholders,
placeholderDescriptions: package.placeholderDescriptions,
lastUpdated: DateTime.now(),
);
}
if (updated) {
emit(state.copyWith(
promptPackages: updatedPackages,
errorMessage: null,
));
AppLogger.i(_tag, '本地状态已更新,默认模板状态已设置');
} else {
AppLogger.w(_tag, '未找到需要更新的功能类型包: ${event.featureType}');
// 如果找不到对应的包则fallback到刷新数据
add(const RefreshPromptData());
}
} catch (error) {
AppLogger.e(_tag, '设置默认模板失败', error);
emit(state.copyWith(
errorMessage: '设置默认模板失败: ${error.toString()}',
));
}
}
}

View File

@@ -0,0 +1,134 @@
import 'package:ainoval/models/prompt_models.dart';
import 'package:equatable/equatable.dart';
/// 提示词管理事件基类
abstract class PromptNewEvent extends Equatable {
const PromptNewEvent();
@override
List<Object?> get props => [];
}
/// 加载所有提示词包
class LoadAllPromptPackages extends PromptNewEvent {
const LoadAllPromptPackages();
}
/// 选择提示词
class SelectPrompt extends PromptNewEvent {
final String promptId;
final AIFeatureType featureType;
const SelectPrompt({
required this.promptId,
required this.featureType,
});
@override
List<Object?> get props => [promptId, featureType];
}
/// 创建新提示词
class CreateNewPrompt extends PromptNewEvent {
final AIFeatureType featureType;
const CreateNewPrompt({
required this.featureType,
});
@override
List<Object?> get props => [featureType];
}
/// 更新提示词详情
class UpdatePromptDetails extends PromptNewEvent {
final String promptId;
final UpdatePromptTemplateRequest request;
const UpdatePromptDetails({
required this.promptId,
required this.request,
});
@override
List<Object?> get props => [promptId, request];
}
/// 复制提示词模板
class CopyPromptTemplate extends PromptNewEvent {
final String templateId;
const CopyPromptTemplate({
required this.templateId,
});
@override
List<Object?> get props => [templateId];
}
/// 切换收藏状态
class ToggleFavoriteStatus extends PromptNewEvent {
final String promptId;
final bool isFavorite;
const ToggleFavoriteStatus({
required this.promptId,
required this.isFavorite,
});
@override
List<Object?> get props => [promptId, isFavorite];
}
/// 设置默认提示词模板
class SetDefaultTemplate extends PromptNewEvent {
final String promptId;
final AIFeatureType featureType;
const SetDefaultTemplate({
required this.promptId,
required this.featureType,
});
@override
List<Object?> get props => [promptId, featureType];
}
/// 删除提示词
class DeletePrompt extends PromptNewEvent {
final String promptId;
const DeletePrompt({
required this.promptId,
});
@override
List<Object?> get props => [promptId];
}
/// 搜索提示词
class SearchPrompts extends PromptNewEvent {
final String query;
const SearchPrompts({
required this.query,
});
@override
List<Object?> get props => [query];
}
/// 清除搜索
class ClearSearch extends PromptNewEvent {
const ClearSearch();
}
/// 切换视图模式
class ToggleViewMode extends PromptNewEvent {
const ToggleViewMode();
}
/// 刷新提示词数据
class RefreshPromptData extends PromptNewEvent {
const RefreshPromptData();
}

View File

@@ -0,0 +1,242 @@
import 'package:ainoval/models/prompt_models.dart';
import 'package:equatable/equatable.dart';
/// 提示词视图模式
enum PromptViewMode {
list,
detail,
}
/// 提示词状态枚举
enum PromptNewStatus {
initial,
loading,
success,
failure,
}
/// 提示词管理状态
class PromptNewState extends Equatable {
const PromptNewState({
this.status = PromptNewStatus.initial,
this.promptPackages = const {},
this.selectedPromptId,
this.selectedFeatureType,
this.viewMode = PromptViewMode.list,
this.searchQuery = '',
this.filteredPrompts = const {},
this.errorMessage,
this.isCreating = false,
this.isUpdating = false,
});
/// 加载状态
final PromptNewStatus status;
/// 提示词包数据
final Map<AIFeatureType, PromptPackage> promptPackages;
/// 当前选中的提示词ID
final String? selectedPromptId;
/// 当前选中的功能类型
final AIFeatureType? selectedFeatureType;
/// 视图模式
final PromptViewMode viewMode;
/// 搜索查询
final String searchQuery;
/// 过滤后的提示词
final Map<AIFeatureType, List<UserPromptInfo>> filteredPrompts;
/// 错误信息
final String? errorMessage;
/// 是否正在创建
final bool isCreating;
/// 是否正在更新
final bool isUpdating;
/// 获取当前选中的提示词
UserPromptInfo? get selectedPrompt {
if (selectedPromptId == null || selectedFeatureType == null) return null;
final package = promptPackages[selectedFeatureType];
if (package == null) return null;
// 获取包含所有类型提示词的完整列表(与列表视图逻辑一致)
final allPrompts = _getAllPromptsForFeatureType(selectedFeatureType!, package);
try {
return allPrompts.firstWhere(
(prompt) => prompt.id == selectedPromptId,
);
} catch (e) {
// 如果找不到选中的提示词,返回第一个可用的提示词
return allPrompts.isNotEmpty ? allPrompts.first : null;
}
}
/// 获取指定功能类型的所有提示词(系统默认 + 用户自定义 + 公开模板)
List<UserPromptInfo> _getAllPromptsForFeatureType(AIFeatureType featureType, PromptPackage package) {
final allPrompts = <UserPromptInfo>[];
// 检查是否有用户默认模板
final hasUserDefault = package.userPrompts.any((prompt) => prompt.isDefault);
// 1. 添加系统默认提示词
if (package.systemPrompt.defaultSystemPrompt.isNotEmpty) {
final systemPromptAsUser = UserPromptInfo(
id: 'system_default_${featureType.toString()}',
name: '系统默认模板',
description: '系统提供的默认提示词模板',
featureType: featureType,
systemPrompt: package.systemPrompt.effectivePrompt,
userPrompt: package.systemPrompt.defaultUserPrompt,
tags: const ['系统默认'],
isDefault: !hasUserDefault, // 当没有用户默认模板时,系统默认模板显示为默认
authorId: 'system',
createdAt: package.lastUpdated,
updatedAt: package.lastUpdated,
);
allPrompts.add(systemPromptAsUser);
}
// 2. 添加用户自定义提示词
allPrompts.addAll(package.userPrompts);
// 3. 添加公开提示词
for (final publicPrompt in package.publicPrompts) {
final publicPromptAsUser = UserPromptInfo(
id: 'public_${publicPrompt.id}',
name: '${publicPrompt.name} ${publicPrompt.isVerified ? '' : ''}',
description: '${publicPrompt.description ?? ''} (作者: ${publicPrompt.authorName ?? '匿名'})',
featureType: featureType,
systemPrompt: publicPrompt.systemPrompt,
userPrompt: publicPrompt.userPrompt,
tags: const ['公开模板'],
categories: publicPrompt.categories,
isPublic: true,
shareCode: publicPrompt.shareCode,
isVerified: publicPrompt.isVerified,
usageCount: publicPrompt.usageCount.toInt(),
favoriteCount: publicPrompt.favoriteCount.toInt(),
rating: publicPrompt.rating ?? 0.0,
authorId: publicPrompt.authorName,
version: publicPrompt.version,
language: publicPrompt.language,
createdAt: publicPrompt.createdAt,
lastUsedAt: publicPrompt.lastUsedAt,
updatedAt: publicPrompt.updatedAt,
);
allPrompts.add(publicPromptAsUser);
}
return allPrompts;
}
/// 获取所有提示词的扁平列表(包含系统默认、用户自定义和公开模板)
List<UserPromptInfo> get allUserPrompts {
final allPrompts = <UserPromptInfo>[];
for (final entry in promptPackages.entries) {
allPrompts.addAll(_getAllPromptsForFeatureType(entry.key, entry.value));
}
return allPrompts;
}
/// 获取所有公开提示词的扁平列表
List<PublicPromptInfo> get allPublicPrompts {
final allPrompts = <PublicPromptInfo>[];
for (final package in promptPackages.values) {
allPrompts.addAll(package.publicPrompts);
}
return allPrompts;
}
/// 检查是否有数据
bool get hasData => promptPackages.isNotEmpty;
/// 检查是否正在加载
bool get isLoading => status == PromptNewStatus.loading;
/// 检查是否加载成功
bool get isSuccess => status == PromptNewStatus.success;
/// 检查是否有错误
bool get hasError => status == PromptNewStatus.failure;
/// 获取指定功能类型的用户提示词
List<UserPromptInfo> getUserPrompts(AIFeatureType featureType) {
return promptPackages[featureType]?.userPrompts ?? [];
}
/// 获取指定功能类型的公开提示词
List<PublicPromptInfo> getPublicPrompts(AIFeatureType featureType) {
return promptPackages[featureType]?.publicPrompts ?? [];
}
/// 获取指定功能类型的系统提示词信息
SystemPromptInfo? getSystemPromptInfo(AIFeatureType featureType) {
return promptPackages[featureType]?.systemPrompt;
}
/// 复制状态
PromptNewState copyWith({
PromptNewStatus? status,
Map<AIFeatureType, PromptPackage>? promptPackages,
String? selectedPromptId,
AIFeatureType? selectedFeatureType,
PromptViewMode? viewMode,
String? searchQuery,
Map<AIFeatureType, List<UserPromptInfo>>? filteredPrompts,
String? errorMessage,
bool? isCreating,
bool? isUpdating,
}) {
return PromptNewState(
status: status ?? this.status,
promptPackages: promptPackages ?? this.promptPackages,
selectedPromptId: selectedPromptId ?? this.selectedPromptId,
selectedFeatureType: selectedFeatureType ?? this.selectedFeatureType,
viewMode: viewMode ?? this.viewMode,
searchQuery: searchQuery ?? this.searchQuery,
filteredPrompts: filteredPrompts ?? this.filteredPrompts,
errorMessage: errorMessage,
isCreating: isCreating ?? this.isCreating,
isUpdating: isUpdating ?? this.isUpdating,
);
}
/// 清除选择状态
PromptNewState clearSelection() {
return copyWith(
selectedPromptId: null,
selectedFeatureType: null,
viewMode: PromptViewMode.list,
);
}
/// 清除错误状态
PromptNewState clearError() {
return copyWith(
errorMessage: null,
);
}
@override
List<Object?> get props => [
status,
promptPackages,
selectedPromptId,
selectedFeatureType,
viewMode,
searchQuery,
filteredPrompts,
errorMessage,
isCreating,
isUpdating,
];
}

View File

@@ -0,0 +1,62 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../models/public_model_config.dart';
import '../../services/api_service/repositories/public_model_repository.dart';
import '../../utils/logger.dart';
part 'public_models_event.dart';
part 'public_models_state.dart';
/// 公共模型BLoC
/// 负责管理公共模型池的状态和数据获取
class PublicModelsBloc extends Bloc<PublicModelsEvent, PublicModelsState> {
final PublicModelRepository _repository;
static const String _tag = 'PublicModelsBloc';
PublicModelsBloc({required PublicModelRepository repository})
: _repository = repository,
super(const PublicModelsInitial()) {
on<LoadPublicModels>(_onLoadPublicModels);
on<RefreshPublicModels>(_onRefreshPublicModels);
}
/// 处理加载公共模型列表事件
Future<void> _onLoadPublicModels(
LoadPublicModels event,
Emitter<PublicModelsState> emit,
) async {
emit(const PublicModelsLoading());
await _loadModels(emit);
}
/// 处理刷新公共模型列表事件
Future<void> _onRefreshPublicModels(
RefreshPublicModels event,
Emitter<PublicModelsState> emit,
) async {
// 刷新不显示loading状态保持当前显示
await _loadModels(emit);
}
/// 加载模型列表的公共方法
Future<void> _loadModels(Emitter<PublicModelsState> emit) async {
try {
AppLogger.i(_tag, '开始加载公共模型列表');
final models = await _repository.getPublicModels();
// 按优先级排序,优先级高的在前
models.sort((a, b) {
final aPriority = a.priority ?? 0;
final bPriority = b.priority ?? 0;
return bPriority.compareTo(aPriority);
});
AppLogger.i(_tag, '公共模型列表加载成功: 共${models.length}个模型');
emit(PublicModelsLoaded(models: models));
} catch (e, stackTrace) {
AppLogger.e(_tag, '加载公共模型列表失败', e, stackTrace);
emit(PublicModelsError(message: '加载公共模型列表失败: ${e.toString()}'));
}
}
}

View File

@@ -0,0 +1,19 @@
part of 'public_models_bloc.dart';
/// 公共模型事件基类
abstract class PublicModelsEvent extends Equatable {
const PublicModelsEvent();
@override
List<Object?> get props => [];
}
/// 加载公共模型列表事件
class LoadPublicModels extends PublicModelsEvent {
const LoadPublicModels();
}
/// 刷新公共模型列表事件
class RefreshPublicModels extends PublicModelsEvent {
const RefreshPublicModels();
}

View File

@@ -0,0 +1,48 @@
part of 'public_models_bloc.dart';
/// 公共模型状态基类
abstract class PublicModelsState extends Equatable {
const PublicModelsState();
@override
List<Object?> get props => [];
}
/// 公共模型初始状态
class PublicModelsInitial extends PublicModelsState {
const PublicModelsInitial();
}
/// 公共模型加载中状态
class PublicModelsLoading extends PublicModelsState {
const PublicModelsLoading();
}
/// 公共模型加载成功状态
class PublicModelsLoaded extends PublicModelsState {
final List<PublicModel> models;
const PublicModelsLoaded({required this.models});
@override
List<Object?> get props => [models];
/// 创建副本,用于更新状态
PublicModelsLoaded copyWith({
List<PublicModel>? models,
}) {
return PublicModelsLoaded(
models: models ?? this.models,
);
}
}
/// 公共模型加载失败状态
class PublicModelsError extends PublicModelsState {
final String message;
const PublicModelsError({required this.message});
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,905 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:ainoval/models/novel_setting_item.dart';
import 'package:ainoval/models/setting_group.dart';
import 'package:ainoval/services/api_service/repositories/novel_setting_repository.dart';
import 'package:ainoval/utils/logger.dart';
// 事件
abstract class SettingEvent extends Equatable {
const SettingEvent();
@override
List<Object?> get props => [];
}
// 加载设定组列表事件
class LoadSettingGroups extends SettingEvent {
final String novelId;
const LoadSettingGroups(this.novelId);
@override
List<Object?> get props => [novelId];
}
// 加载设定条目列表事件
class LoadSettingItems extends SettingEvent {
final String novelId;
final String? groupId;
final String? type;
final String? name;
final int page;
final int size;
const LoadSettingItems({
required this.novelId,
this.groupId,
this.type,
this.name,
this.page = 0,
this.size = 500, // 🔧 修复增加到500以支持大量设定显示
});
@override
List<Object?> get props => [novelId, groupId, type, name, page, size];
}
// 创建设定组事件
class CreateSettingGroup extends SettingEvent {
final String novelId;
final SettingGroup group;
const CreateSettingGroup({
required this.novelId,
required this.group,
});
@override
List<Object?> get props => [novelId, group];
}
// 更新设定组事件
class UpdateSettingGroup extends SettingEvent {
final String novelId;
final String groupId;
final SettingGroup group;
const UpdateSettingGroup({
required this.novelId,
required this.groupId,
required this.group,
});
@override
List<Object?> get props => [novelId, groupId, group];
}
// 删除设定组事件
class DeleteSettingGroup extends SettingEvent {
final String novelId;
final String groupId;
const DeleteSettingGroup({
required this.novelId,
required this.groupId,
});
@override
List<Object?> get props => [novelId, groupId];
}
// 设置设定组激活状态事件
class SetGroupActiveContext extends SettingEvent {
final String novelId;
final String groupId;
final bool isActive;
const SetGroupActiveContext({
required this.novelId,
required this.groupId,
required this.isActive,
});
@override
List<Object?> get props => [novelId, groupId, isActive];
}
// 创建设定条目事件
class CreateSettingItem extends SettingEvent {
final String novelId;
final NovelSettingItem item;
final String? groupId;
const CreateSettingItem({
required this.novelId,
required this.item,
this.groupId,
});
@override
List<Object?> get props => [novelId, item, groupId];
}
// 更新设定条目事件
class UpdateSettingItem extends SettingEvent {
final String novelId;
final String itemId;
final NovelSettingItem item;
const UpdateSettingItem({
required this.novelId,
required this.itemId,
required this.item,
});
@override
List<Object?> get props => [novelId, itemId, item];
}
// 删除设定条目事件
class DeleteSettingItem extends SettingEvent {
final String novelId;
final String itemId;
const DeleteSettingItem({
required this.novelId,
required this.itemId,
});
@override
List<Object?> get props => [novelId, itemId];
}
// 添加条目到设定组事件
class AddItemToGroup extends SettingEvent {
final String novelId;
final String groupId;
final String itemId;
const AddItemToGroup({
required this.novelId,
required this.groupId,
required this.itemId,
});
@override
List<Object?> get props => [novelId, groupId, itemId];
}
// 从设定组移除条目事件
class RemoveItemFromGroup extends SettingEvent {
final String novelId;
final String groupId;
final String itemId;
const RemoveItemFromGroup({
required this.novelId,
required this.groupId,
required this.itemId,
});
@override
List<Object?> get props => [novelId, groupId, itemId];
}
// 添加设定条目关系事件
class AddSettingRelationship extends SettingEvent {
final String novelId;
final String itemId;
final String targetItemId;
final String relationshipType;
final String? description;
const AddSettingRelationship({
required this.novelId,
required this.itemId,
required this.targetItemId,
required this.relationshipType,
this.description,
});
@override
List<Object?> get props => [novelId, itemId, targetItemId, relationshipType, description];
}
// 删除设定条目关系事件
class RemoveSettingRelationship extends SettingEvent {
final String novelId;
final String itemId;
final String targetItemId;
final String relationshipType;
const RemoveSettingRelationship({
required this.novelId,
required this.itemId,
required this.targetItemId,
required this.relationshipType,
});
@override
List<Object?> get props => [novelId, itemId, targetItemId, relationshipType];
}
// 设置父子关系事件
class SetParentChildRelationship extends SettingEvent {
final String novelId;
final String childId;
final String parentId;
const SetParentChildRelationship({
required this.novelId,
required this.childId,
required this.parentId,
});
@override
List<Object?> get props => [novelId, childId, parentId];
}
// 移除父子关系事件
class RemoveParentChildRelationship extends SettingEvent {
final String novelId;
final String childId;
const RemoveParentChildRelationship({
required this.novelId,
required this.childId,
});
@override
List<Object?> get props => [novelId, childId];
}
// 创建设定条目并添加到组事件
class CreateSettingItemAndAddToGroup extends SettingEvent {
final String novelId;
final NovelSettingItem item;
final String groupId;
const CreateSettingItemAndAddToGroup({
required this.novelId,
required this.item,
required this.groupId,
});
@override
List<Object?> get props => [novelId, item, groupId];
}
// 状态
enum SettingStatus { initial, loading, success, failure }
class SettingState extends Equatable {
final SettingStatus groupsStatus;
final SettingStatus itemsStatus;
final List<SettingGroup> groups;
final List<NovelSettingItem> items;
final String? selectedGroupId;
final String? error;
const SettingState({
this.groupsStatus = SettingStatus.initial,
this.itemsStatus = SettingStatus.initial,
this.groups = const [],
this.items = const [],
this.selectedGroupId,
this.error,
});
SettingState copyWith({
SettingStatus? groupsStatus,
SettingStatus? itemsStatus,
List<SettingGroup>? groups,
List<NovelSettingItem>? items,
String? selectedGroupId,
String? error,
}) {
return SettingState(
groupsStatus: groupsStatus ?? this.groupsStatus,
itemsStatus: itemsStatus ?? this.itemsStatus,
groups: groups ?? this.groups,
items: items ?? this.items,
selectedGroupId: selectedGroupId ?? this.selectedGroupId,
error: error ?? this.error,
);
}
@override
List<Object?> get props => [groupsStatus, itemsStatus, groups, items, selectedGroupId, error];
}
// Bloc
class SettingBloc extends Bloc<SettingEvent, SettingState> {
final NovelSettingRepository settingRepository;
SettingBloc({required this.settingRepository}) : super(const SettingState()) {
on<LoadSettingGroups>(_onLoadSettingGroups);
on<LoadSettingItems>(_onLoadSettingItems);
on<CreateSettingGroup>(_onCreateSettingGroup);
on<UpdateSettingGroup>(_onUpdateSettingGroup);
on<DeleteSettingGroup>(_onDeleteSettingGroup);
on<SetGroupActiveContext>(_onSetGroupActiveContext);
on<CreateSettingItem>(_onCreateSettingItem);
on<UpdateSettingItem>(_onUpdateSettingItem);
on<DeleteSettingItem>(_onDeleteSettingItem);
on<AddItemToGroup>(_onAddItemToGroup);
on<RemoveItemFromGroup>(_onRemoveItemFromGroup);
on<AddSettingRelationship>(_onAddSettingRelationship);
on<RemoveSettingRelationship>(_onRemoveSettingRelationship);
on<SetParentChildRelationship>(_onSetParentChildRelationship);
on<RemoveParentChildRelationship>(_onRemoveParentChildRelationship);
on<CreateSettingItemAndAddToGroup>(_onCreateSettingItemAndAddToGroup);
}
Future<void> _onLoadSettingGroups(
LoadSettingGroups event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(groupsStatus: SettingStatus.loading));
final groups = await settingRepository.getNovelSettingGroups(
novelId: event.novelId,
);
emit(state.copyWith(
groupsStatus: SettingStatus.success,
groups: groups,
));
} catch (e) {
AppLogger.e('SettingBloc', '加载设定组失败', e);
emit(state.copyWith(
groupsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onLoadSettingItems(
LoadSettingItems event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(
itemsStatus: SettingStatus.loading,
selectedGroupId: event.groupId,
));
final items = await settingRepository.getNovelSettingItems(
novelId: event.novelId,
type: event.type,
name: event.name,
page: event.page,
size: event.size,
sortBy: 'name',
sortDirection: 'asc',
);
emit(state.copyWith(
itemsStatus: SettingStatus.success,
items: items,
));
} catch (e) {
AppLogger.e('SettingBloc', '加载设定条目失败', e);
emit(state.copyWith(
itemsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onCreateSettingGroup(
CreateSettingGroup event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(groupsStatus: SettingStatus.loading));
final createdGroup = await settingRepository.createSettingGroup(
novelId: event.novelId,
settingGroup: event.group,
);
// 更新列表,添加新组
final updatedGroups = List<SettingGroup>.from(state.groups)..add(createdGroup);
emit(state.copyWith(
groupsStatus: SettingStatus.success,
groups: updatedGroups,
));
} catch (e) {
AppLogger.e('SettingBloc', '创建设定组失败', e);
emit(state.copyWith(
groupsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onUpdateSettingGroup(
UpdateSettingGroup event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(groupsStatus: SettingStatus.loading));
final updatedGroup = await settingRepository.updateSettingGroup(
novelId: event.novelId,
groupId: event.groupId,
settingGroup: event.group,
);
// 更新列表,替换更新的组
final updatedGroups = state.groups.map((group) {
return group.id == event.groupId ? updatedGroup : group;
}).toList();
emit(state.copyWith(
groupsStatus: SettingStatus.success,
groups: updatedGroups,
));
} catch (e) {
AppLogger.e('SettingBloc', '更新设定组失败', e);
emit(state.copyWith(
groupsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onDeleteSettingGroup(
DeleteSettingGroup event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(groupsStatus: SettingStatus.loading));
await settingRepository.deleteSettingGroup(
novelId: event.novelId,
groupId: event.groupId,
);
// 更新列表,移除删除的组
final updatedGroups = state.groups.where((group) => group.id != event.groupId).toList();
// 如果删除的是当前选中的组,清除选中状态
final selectedGroupId = state.selectedGroupId == event.groupId ? null : state.selectedGroupId;
emit(state.copyWith(
groupsStatus: SettingStatus.success,
groups: updatedGroups,
selectedGroupId: selectedGroupId,
));
} catch (e) {
AppLogger.e('SettingBloc', '删除设定组失败', e);
emit(state.copyWith(
groupsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onSetGroupActiveContext(
SetGroupActiveContext event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(groupsStatus: SettingStatus.loading));
final updatedGroup = await settingRepository.setGroupActiveContext(
novelId: event.novelId,
groupId: event.groupId,
isActive: event.isActive,
);
// 更新列表,替换更新的组
final updatedGroups = state.groups.map((group) {
return group.id == event.groupId ? updatedGroup : group;
}).toList();
emit(state.copyWith(
groupsStatus: SettingStatus.success,
groups: updatedGroups,
));
} catch (e) {
AppLogger.e('SettingBloc', '设置设定组激活状态失败', e);
emit(state.copyWith(
groupsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onCreateSettingItem(
CreateSettingItem event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(itemsStatus: SettingStatus.loading));
final createdItem = await settingRepository.createSettingItem(
novelId: event.novelId,
settingItem: event.item,
);
// 确保createdItem有有效ID
if (createdItem.id != null && createdItem.id!.isNotEmpty) {
// 更新列表,添加新条目
final updatedItems = List<NovelSettingItem>.from(state.items)..add(createdItem);
// 按名称排序确保UI一致性
updatedItems.sort((a, b) => a.name.compareTo(b.name));
emit(state.copyWith(
itemsStatus: SettingStatus.success,
items: updatedItems,
));
// 记录日志
AppLogger.i('SettingBloc', '成功添加设定条目到本地状态: id=${createdItem.id}, name=${createdItem.name}');
// 重要修改不再在这里调用add(AddItemToGroup),而是通过专门的合并事件处理
// 这样避免了BLoC关闭后无法添加新事件的问题
} else {
// 如果没有有效ID重新加载整个列表
AppLogger.w('SettingBloc', '创建的设定条目没有有效ID将重新加载列表');
final items = await settingRepository.getNovelSettingItems(
novelId: event.novelId,
page: 0, // 🔧 修复:保持从第一页开始
size: 500, // 🔧 修复增加到500以支持大量设定显示
sortBy: 'name',
sortDirection: 'asc',
);
emit(state.copyWith(
itemsStatus: SettingStatus.success,
items: items,
));
}
} catch (e) {
AppLogger.e('SettingBloc', '创建设定条目失败', e);
emit(state.copyWith(
itemsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onUpdateSettingItem(
UpdateSettingItem event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(itemsStatus: SettingStatus.loading));
final updatedItem = await settingRepository.updateSettingItem(
novelId: event.novelId,
itemId: event.itemId,
settingItem: event.item,
);
// 更新列表,替换更新的条目
final updatedItems = state.items.map((item) {
return item.id == event.itemId ? updatedItem : item;
}).toList();
emit(state.copyWith(
itemsStatus: SettingStatus.success,
items: updatedItems,
));
} catch (e) {
AppLogger.e('SettingBloc', '更新设定条目失败', e);
emit(state.copyWith(
itemsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onDeleteSettingItem(
DeleteSettingItem event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(itemsStatus: SettingStatus.loading));
await settingRepository.deleteSettingItem(
novelId: event.novelId,
itemId: event.itemId,
);
// 更新列表,移除删除的条目
final updatedItems = state.items.where((item) => item.id != event.itemId).toList();
emit(state.copyWith(
itemsStatus: SettingStatus.success,
items: updatedItems,
));
} catch (e) {
AppLogger.e('SettingBloc', '删除设定条目失败', e);
emit(state.copyWith(
itemsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onAddItemToGroup(
AddItemToGroup event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(groupsStatus: SettingStatus.loading));
final updatedGroup = await settingRepository.addItemToGroup(
novelId: event.novelId,
groupId: event.groupId,
itemId: event.itemId,
);
// 更新列表,替换更新的组
final updatedGroups = state.groups.map((group) {
return group.id == event.groupId ? updatedGroup : group;
}).toList();
emit(state.copyWith(
groupsStatus: SettingStatus.success,
groups: updatedGroups,
));
} catch (e) {
AppLogger.e('SettingBloc', '添加条目到设定组失败', e);
emit(state.copyWith(
groupsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onRemoveItemFromGroup(
RemoveItemFromGroup event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(groupsStatus: SettingStatus.loading));
await settingRepository.removeItemFromGroup(
novelId: event.novelId,
groupId: event.groupId,
itemId: event.itemId,
);
// 重新加载设定组列表以获取更新后的状态
final updatedGroups = await settingRepository.getNovelSettingGroups(
novelId: event.novelId,
);
emit(state.copyWith(
groupsStatus: SettingStatus.success,
groups: updatedGroups,
));
} catch (e) {
AppLogger.e('SettingBloc', '从设定组移除条目失败', e);
emit(state.copyWith(
groupsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onAddSettingRelationship(
AddSettingRelationship event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(itemsStatus: SettingStatus.loading));
final updatedItem = await settingRepository.addSettingRelationship(
novelId: event.novelId,
itemId: event.itemId,
targetItemId: event.targetItemId,
relationshipType: event.relationshipType,
description: event.description,
);
// 更新列表,替换更新的条目
final updatedItems = state.items.map((item) {
return item.id == event.itemId ? updatedItem : item;
}).toList();
emit(state.copyWith(
itemsStatus: SettingStatus.success,
items: updatedItems,
));
} catch (e) {
AppLogger.e('SettingBloc', '添加设定条目关系失败', e);
emit(state.copyWith(
itemsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onRemoveSettingRelationship(
RemoveSettingRelationship event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(itemsStatus: SettingStatus.loading));
await settingRepository.removeSettingRelationship(
novelId: event.novelId,
itemId: event.itemId,
targetItemId: event.targetItemId,
relationshipType: event.relationshipType,
);
// 重新加载该设定条目以获取更新后的状态
final updatedItem = await settingRepository.getSettingItemDetail(
novelId: event.novelId,
itemId: event.itemId,
);
// 更新列表,替换更新的条目
final updatedItems = state.items.map((item) {
return item.id == event.itemId ? updatedItem : item;
}).toList();
emit(state.copyWith(
itemsStatus: SettingStatus.success,
items: updatedItems,
));
} catch (e) {
AppLogger.e('SettingBloc', '删除设定条目关系失败', e);
emit(state.copyWith(
itemsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onCreateSettingItemAndAddToGroup(
CreateSettingItemAndAddToGroup event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(itemsStatus: SettingStatus.loading));
AppLogger.i('SettingBloc', '创建设定条目并添加到组: groupId=${event.groupId}');
// 1. 创建设定条目
final createdItem = await settingRepository.createSettingItem(
novelId: event.novelId,
settingItem: event.item,
);
// 确保createdItem有有效ID
if (createdItem.id != null && createdItem.id!.isNotEmpty) {
// 2. 将设定条目添加到组
final updatedGroup = await settingRepository.addItemToGroup(
novelId: event.novelId,
groupId: event.groupId,
itemId: createdItem.id!,
);
// 3. 更新状态 - 同时更新条目列表和组列表
final updatedItems = List<NovelSettingItem>.from(state.items)..add(createdItem);
// 按名称排序确保UI一致性
updatedItems.sort((a, b) => a.name.compareTo(b.name));
// 更新组列表
final updatedGroups = state.groups.map((group) {
return group.id == event.groupId ? updatedGroup : group;
}).toList();
emit(state.copyWith(
itemsStatus: SettingStatus.success,
groupsStatus: SettingStatus.success,
items: updatedItems,
groups: updatedGroups,
));
AppLogger.i('SettingBloc', '成功创建设定条目并添加到组: id=${createdItem.id}, name=${createdItem.name}, groupId=${event.groupId}');
} else {
// 如果没有有效ID重新加载整个列表
AppLogger.w('SettingBloc', '创建的设定条目没有有效ID将重新加载列表');
// 并行加载条目和组
final items = await settingRepository.getNovelSettingItems(
novelId: event.novelId,
page: 0, // 🔧 修复:保持从第一页开始
size: 500, // 🔧 修复增加到500以支持大量设定显示
sortBy: 'name',
sortDirection: 'asc',
);
final groups = await settingRepository.getNovelSettingGroups(
novelId: event.novelId,
);
emit(state.copyWith(
itemsStatus: SettingStatus.success,
groupsStatus: SettingStatus.success,
items: items,
groups: groups,
));
}
} catch (e) {
AppLogger.e('SettingBloc', '创建设定条目并添加到组失败', e);
emit(state.copyWith(
itemsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onSetParentChildRelationship(
SetParentChildRelationship event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(itemsStatus: SettingStatus.loading));
await settingRepository.setParentChildRelationship(
novelId: event.novelId,
childId: event.childId,
parentId: event.parentId,
);
// 重新加载整个设定条目列表以确保父子关系状态正确
final updatedItems = await settingRepository.getNovelSettingItems(
novelId: event.novelId,
page: 0, // 🔧 修复:保持从第一页开始
size: 100, // 加载更多条目以确保完整性
sortBy: 'name',
sortDirection: 'asc',
);
emit(state.copyWith(
itemsStatus: SettingStatus.success,
items: updatedItems,
));
} catch (e) {
AppLogger.e('SettingBloc', '设置父子关系失败', e);
emit(state.copyWith(
itemsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
Future<void> _onRemoveParentChildRelationship(
RemoveParentChildRelationship event,
Emitter<SettingState> emit,
) async {
try {
emit(state.copyWith(itemsStatus: SettingStatus.loading));
await settingRepository.removeParentChildRelationship(
novelId: event.novelId,
childId: event.childId,
);
// 重新加载整个设定条目列表以确保父子关系状态正确
final updatedItems = await settingRepository.getNovelSettingItems(
novelId: event.novelId,
page: 0, // 🔧 修复:保持从第一页开始
size: 100, // 加载更多条目以确保完整性
sortBy: 'name',
sortDirection: 'asc',
);
emit(state.copyWith(
itemsStatus: SettingStatus.success,
items: updatedItems,
));
} catch (e) {
AppLogger.e('SettingBloc', '移除父子关系失败', e);
emit(state.copyWith(
itemsStatus: SettingStatus.failure,
error: e.toString(),
));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,482 @@
import 'package:equatable/equatable.dart';
abstract class SettingGenerationBlocEvent extends Equatable {
const SettingGenerationBlocEvent();
@override
List<Object?> get props => [];
}
/// 加载可用策略
class LoadStrategiesEvent extends SettingGenerationBlocEvent {
final String? novelId;
final String? userId;
const LoadStrategiesEvent({
this.novelId,
this.userId,
});
@override
List<Object?> get props => [novelId, userId];
}
/// 加载历史记录
class LoadHistoriesEvent extends SettingGenerationBlocEvent {
final String novelId;
final String userId;
final int page;
final int size;
const LoadHistoriesEvent({
required this.novelId,
required this.userId,
this.page = 0,
this.size = 20,
});
@override
List<Object?> get props => [novelId, userId, page, size];
}
/// 从小说设定创建编辑会话
class StartSessionFromNovelEvent extends SettingGenerationBlocEvent {
final String novelId;
final String editReason;
final String modelConfigId;
final bool createNewSnapshot;
const StartSessionFromNovelEvent({
required this.novelId,
required this.editReason,
required this.modelConfigId,
required this.createNewSnapshot,
});
@override
List<Object?> get props => [novelId, editReason, modelConfigId, createNewSnapshot];
}
/// 开始生成设定
class StartGenerationEvent extends SettingGenerationBlocEvent {
final String initialPrompt;
final String promptTemplateId;
final String? novelId;
final String modelConfigId;
final String? userId;
// 文本阶段公共模型透传(仅记录,不改变文本阶段默认使用私有模型)
final bool? usePublicTextModel;
final String? textPhasePublicProvider;
final String? textPhasePublicModelId;
const StartGenerationEvent({
required this.initialPrompt,
required this.promptTemplateId,
this.novelId,
required this.modelConfigId,
this.userId,
this.usePublicTextModel,
this.textPhasePublicProvider,
this.textPhasePublicModelId,
});
@override
List<Object?> get props => [
initialPrompt,
promptTemplateId,
novelId,
modelConfigId,
userId,
usePublicTextModel,
textPhasePublicProvider,
textPhasePublicModelId,
];
}
/// 基于当前会话进行整体调整生成
class AdjustGenerationEvent extends SettingGenerationBlocEvent {
final String sessionId;
final String adjustmentPrompt;
final String modelConfigId;
final String? promptTemplateId;
const AdjustGenerationEvent({
required this.sessionId,
required this.adjustmentPrompt,
required this.modelConfigId,
this.promptTemplateId,
});
@override
List<Object?> get props => [sessionId, adjustmentPrompt, modelConfigId, promptTemplateId];
}
/// 修改节点
class UpdateNodeEvent extends SettingGenerationBlocEvent {
final String nodeId;
final String modificationPrompt;
final String modelConfigId;
final String scope; // 'self' | 'self_and_children' | 'children_only'
const UpdateNodeEvent({
required this.nodeId,
required this.modificationPrompt,
required this.modelConfigId,
this.scope = 'self',
});
@override
List<Object?> get props => [
nodeId,
modificationPrompt,
modelConfigId,
scope,
];
}
/// 选择节点
class SelectNodeEvent extends SettingGenerationBlocEvent {
final String? nodeId;
const SelectNodeEvent(this.nodeId);
@override
List<Object?> get props => [nodeId];
}
/// 切换视图模式
class ToggleViewModeEvent extends SettingGenerationBlocEvent {
final String viewMode; // 'compact' | 'detailed'
const ToggleViewModeEvent(this.viewMode);
@override
List<Object?> get props => [viewMode];
}
/// 应用待处理的更改
class ApplyPendingChangesEvent extends SettingGenerationBlocEvent {
const ApplyPendingChangesEvent();
}
/// 取消待处理的更改
class CancelPendingChangesEvent extends SettingGenerationBlocEvent {
const CancelPendingChangesEvent();
}
/// 撤销节点更改
class UndoNodeChangeEvent extends SettingGenerationBlocEvent {
final String nodeId;
const UndoNodeChangeEvent(this.nodeId);
@override
List<Object?> get props => [nodeId];
}
/// 保存生成的设定
class SaveGeneratedSettingsEvent extends SettingGenerationBlocEvent {
final String? novelId; // 改为可空,支持独立快照
final bool updateExisting; // 是否更新现有历史记录
final String? targetHistoryId; // 目标历史记录ID
const SaveGeneratedSettingsEvent(
this.novelId, {
this.updateExisting = false,
this.targetHistoryId,
});
@override
List<Object?> get props => [novelId, updateExisting, targetHistoryId];
}
/// 创建新会话
class CreateNewSessionEvent extends SettingGenerationBlocEvent {
const CreateNewSessionEvent();
}
/// 选择会话
class SelectSessionEvent extends SettingGenerationBlocEvent {
final String sessionId;
final bool isHistorySession;
const SelectSessionEvent(
this.sessionId, {
this.isHistorySession = false,
});
@override
List<Object?> get props => [sessionId, isHistorySession];
}
/// 从历史记录创建编辑会话
class CreateSessionFromHistoryEvent extends SettingGenerationBlocEvent {
final String historyId;
final String userId;
final String editReason;
final String modelConfigId;
const CreateSessionFromHistoryEvent({
required this.historyId,
required this.userId,
this.editReason = '从历史记录编辑',
required this.modelConfigId,
});
@override
List<Object?> get props => [historyId, userId, editReason, modelConfigId];
}
/// 更新调整提示词
class UpdateAdjustmentPromptEvent extends SettingGenerationBlocEvent {
final String prompt;
const UpdateAdjustmentPromptEvent(this.prompt);
@override
List<Object?> get props => [prompt];
}
/// 重置状态事件
class ResetEvent extends SettingGenerationBlocEvent {
const ResetEvent();
}
/// 重试事件(从错误状态恢复)
class RetryEvent extends SettingGenerationBlocEvent {
const RetryEvent();
}
/// 开始渲染节点事件
class StartNodeRenderEvent extends SettingGenerationBlocEvent {
final String nodeId;
const StartNodeRenderEvent(this.nodeId);
@override
List<Object?> get props => [nodeId];
}
/// 完成节点渲染事件
class CompleteNodeRenderEvent extends SettingGenerationBlocEvent {
final String nodeId;
const CompleteNodeRenderEvent(this.nodeId);
@override
List<Object?> get props => [nodeId];
}
/// 处理渲染队列事件
class ProcessRenderQueueEvent extends SettingGenerationBlocEvent {
const ProcessRenderQueueEvent();
@override
List<Object?> get props => [];
}
/// 更新节点内容事件
class UpdateNodeContentEvent extends SettingGenerationBlocEvent {
final String nodeId;
final String content;
const UpdateNodeContentEvent({
required this.nodeId,
required this.content,
});
@override
List<Object?> get props => [nodeId, content];
}
/// 获取会话状态事件
class GetSessionStatusEvent extends SettingGenerationBlocEvent {
final String sessionId;
const GetSessionStatusEvent(this.sessionId);
@override
List<Object?> get props => [sessionId];
}
/// 取消会话事件
class CancelSessionEvent extends SettingGenerationBlocEvent {
final String sessionId;
const CancelSessionEvent(this.sessionId);
@override
List<Object?> get props => [sessionId];
}
// ==================== NOVEL_COMPOSE 事件族 ====================
/// 启动:只生成大纲
class StartComposeOutlineEvent extends SettingGenerationBlocEvent {
final String? novelId;
final String userId;
final String modelConfigId;
final bool? isPublicModel;
final String? publicModelConfigId;
final String? settingSessionId; // 方案A后端拉取会话转换
final Map<String, dynamic>? contextSelections; // 直接透传已选上下文(可选)
final String? prompt; // 自由提示词
final String? instructions; // 生成指令
final int chapterCount; // 按章大纲数量(支持黄金三章=3
final Map<String, dynamic> parameters; // 其他采样/模式参数
const StartComposeOutlineEvent({
required this.userId,
required this.modelConfigId,
this.isPublicModel,
this.publicModelConfigId,
this.novelId,
this.settingSessionId,
this.contextSelections,
this.prompt,
this.instructions,
this.chapterCount = 3,
this.parameters = const {},
});
}
/// 启动直接生成章节黄金三章或指定N章
class StartComposeChaptersEvent extends SettingGenerationBlocEvent {
final String? novelId;
final String userId;
final String modelConfigId;
final bool? isPublicModel;
final String? publicModelConfigId;
final String? settingSessionId;
final Map<String, dynamic>? contextSelections;
final String? prompt;
final String? instructions;
final int chapterCount; // 生成章节数
final Map<String, dynamic> parameters;
const StartComposeChaptersEvent({
required this.userId,
required this.modelConfigId,
this.isPublicModel,
this.publicModelConfigId,
this.novelId,
this.settingSessionId,
this.contextSelections,
this.prompt,
this.instructions,
this.chapterCount = 3,
this.parameters = const {},
});
}
/// 启动先大纲后章节outline_plus_chapters
class StartComposeBundleEvent extends SettingGenerationBlocEvent {
final String? novelId;
final String userId;
final String modelConfigId;
final bool? isPublicModel;
final String? publicModelConfigId;
final String? settingSessionId;
final Map<String, dynamic>? contextSelections;
final String? prompt;
final String? instructions;
final int chapterCount; // 需要的大纲/章节数量
final Map<String, dynamic> parameters;
const StartComposeBundleEvent({
required this.userId,
required this.modelConfigId,
this.isPublicModel,
this.publicModelConfigId,
this.novelId,
this.settingSessionId,
this.contextSelections,
this.prompt,
this.instructions,
this.chapterCount = 3,
this.parameters = const {},
});
}
/// 微调:针对已生成的大纲或章节进行整体或定向调整
class RefineComposeEvent extends SettingGenerationBlocEvent {
final String? novelId;
final String userId;
final String modelConfigId;
final String? settingSessionId;
final Map<String, dynamic>? contextSelections;
final String? instructions; // 具体微调指令
final Map<String, dynamic> parameters; // 可包含 chapterIndex、outlineText 等
const RefineComposeEvent({
required this.userId,
required this.modelConfigId,
this.novelId,
this.settingSessionId,
this.contextSelections,
this.instructions,
this.parameters = const {},
});
}
/// 取消写作编排流
class CancelComposeEvent extends SettingGenerationBlocEvent {
final String connectionId; // SSE连接ID或业务自定义ID
const CancelComposeEvent(this.connectionId);
@override
List<Object?> get props => [connectionId];
}
/// 获取用户历史记录事件
class GetUserHistoriesEvent extends SettingGenerationBlocEvent {
final String? novelId;
final int page;
final int size;
const GetUserHistoriesEvent({
this.novelId,
this.page = 0,
this.size = 20,
});
@override
List<Object?> get props => [novelId, page, size];
}
/// 删除历史记录事件
class DeleteHistoryEvent extends SettingGenerationBlocEvent {
final String historyId;
const DeleteHistoryEvent(this.historyId);
@override
List<Object?> get props => [historyId];
}
/// 复制历史记录事件
class CopyHistoryEvent extends SettingGenerationBlocEvent {
final String historyId;
final String copyReason;
const CopyHistoryEvent({
required this.historyId,
required this.copyReason,
});
@override
List<Object?> get props => [historyId, copyReason];
}
/// 恢复历史记录到小说事件
class RestoreHistoryToNovelEvent extends SettingGenerationBlocEvent {
final String historyId;
final String novelId;
const RestoreHistoryToNovelEvent({
required this.historyId,
required this.novelId,
});
@override
List<Object?> get props => [historyId, novelId];
}

View File

@@ -0,0 +1,536 @@
import 'package:equatable/equatable.dart';
import '../../models/setting_generation_session.dart';
import '../../models/setting_node.dart';
import '../../models/setting_generation_event.dart' as event_model;
import '../../models/compose_preview.dart';
import '../../models/strategy_template_info.dart';
import '../../utils/setting_node_utils.dart'; // 导入工具类
abstract class SettingGenerationState extends Equatable {
const SettingGenerationState();
@override
List<Object?> get props => [];
}
/// 初始状态
class SettingGenerationInitial extends SettingGenerationState {
const SettingGenerationInitial();
}
/// 加载中
class SettingGenerationLoading extends SettingGenerationState {
final String? message;
const SettingGenerationLoading({this.message});
@override
List<Object?> get props => [message];
}
/// 策略已加载
class StrategiesLoaded extends SettingGenerationState {
final List<StrategyTemplateInfo> strategies;
const StrategiesLoaded(this.strategies);
@override
List<Object?> get props => [strategies];
}
/// 待机状态(准备开始生成)
class SettingGenerationReady extends SettingGenerationState {
final List<StrategyTemplateInfo> strategies;
final List<SettingGenerationSession> sessions;
final String? activeSessionId;
final String adjustmentPrompt;
final String viewMode;
const SettingGenerationReady({
required this.strategies,
this.sessions = const [],
this.activeSessionId,
this.adjustmentPrompt = '',
this.viewMode = 'compact',
});
@override
List<Object?> get props => [
strategies,
sessions,
activeSessionId,
adjustmentPrompt,
viewMode,
];
SettingGenerationReady copyWith({
List<StrategyTemplateInfo>? strategies,
List<SettingGenerationSession>? sessions,
String? activeSessionId,
String? adjustmentPrompt,
String? viewMode,
}) {
return SettingGenerationReady(
strategies: strategies ?? this.strategies,
sessions: sessions ?? this.sessions,
activeSessionId: activeSessionId ?? this.activeSessionId,
adjustmentPrompt: adjustmentPrompt ?? this.adjustmentPrompt,
viewMode: viewMode ?? this.viewMode,
);
}
}
/// 节点渲染状态枚举
enum NodeRenderState {
pending, // 待渲染(在队列中)
rendering, // 正在渲染(动画中)
rendered, // 已渲染完成
}
/// 节点渲染信息
class NodeRenderInfo {
final String nodeId;
final NodeRenderState state;
final DateTime? renderStartTime;
final Duration? renderDuration;
const NodeRenderInfo({
required this.nodeId,
required this.state,
this.renderStartTime,
this.renderDuration,
});
NodeRenderInfo copyWith({
NodeRenderState? state,
DateTime? renderStartTime,
Duration? renderDuration,
}) {
return NodeRenderInfo(
nodeId: nodeId,
state: state ?? this.state,
renderStartTime: renderStartTime ?? this.renderStartTime,
renderDuration: renderDuration ?? this.renderDuration,
);
}
}
/// 生成中
class SettingGenerationInProgress extends SettingGenerationState {
final List<StrategyTemplateInfo> strategies;
final List<SettingGenerationSession> sessions;
final String activeSessionId;
final SettingGenerationSession activeSession;
final String? selectedNodeId;
final String viewMode;
final String adjustmentPrompt;
final Map<String, SettingNode> pendingChanges;
final Set<String> highlightedNodeIds;
final Map<String, List<SettingNode>> editHistory;
final List<event_model.SettingGenerationEvent> events;
final bool isGenerating;
final String? currentOperation;
// 新增:写作编排流的预览缓存(仅前端展示,不落库)
final List<ComposeChapterPreview> composePreview;
// 新增的渲染状态管理字段
final Map<String, NodeRenderInfo> nodeRenderStates;
final List<String> renderQueue;
final Set<String> renderedNodeIds;
final List<event_model.NodeCreatedEvent> pendingNodes;
// 粘性警告(例如余额不足提醒),不会被后续普通事件覆盖
final String? stickyWarning;
const SettingGenerationInProgress({
required this.strategies,
required this.sessions,
required this.activeSessionId,
required this.activeSession,
this.selectedNodeId,
this.viewMode = 'compact',
this.adjustmentPrompt = '',
this.pendingChanges = const {},
this.highlightedNodeIds = const {},
this.editHistory = const {},
this.isGenerating = false,
this.currentOperation,
this.composePreview = const [],
this.events = const [],
this.nodeRenderStates = const {},
this.renderQueue = const [],
this.renderedNodeIds = const {},
this.pendingNodes = const [],
this.stickyWarning,
});
@override
List<Object?> get props => [
strategies,
sessions,
activeSessionId,
activeSession,
selectedNodeId,
viewMode,
adjustmentPrompt,
pendingChanges,
highlightedNodeIds,
editHistory,
isGenerating,
currentOperation,
composePreview,
events,
nodeRenderStates,
renderQueue,
renderedNodeIds,
stickyWarning,
];
SettingGenerationInProgress copyWith({
List<StrategyTemplateInfo>? strategies,
List<SettingGenerationSession>? sessions,
String? activeSessionId,
SettingGenerationSession? activeSession,
String? selectedNodeId,
String? viewMode,
String? adjustmentPrompt,
Map<String, SettingNode>? pendingChanges,
Set<String>? highlightedNodeIds,
Map<String, List<SettingNode>>? editHistory,
bool? isGenerating,
String? currentOperation,
List<ComposeChapterPreview>? composePreview,
List<event_model.SettingGenerationEvent>? events,
Map<String, NodeRenderInfo>? nodeRenderStates,
List<String>? renderQueue,
Set<String>? renderedNodeIds,
List<event_model.NodeCreatedEvent>? pendingNodes,
String? stickyWarning,
}) {
return SettingGenerationInProgress(
strategies: strategies ?? this.strategies,
sessions: sessions ?? this.sessions,
activeSessionId: activeSessionId ?? this.activeSessionId,
activeSession: activeSession ?? this.activeSession,
selectedNodeId: selectedNodeId ?? this.selectedNodeId,
viewMode: viewMode ?? this.viewMode,
adjustmentPrompt: adjustmentPrompt ?? this.adjustmentPrompt,
pendingChanges: pendingChanges ?? this.pendingChanges,
highlightedNodeIds: highlightedNodeIds ?? this.highlightedNodeIds,
editHistory: editHistory ?? this.editHistory,
isGenerating: isGenerating ?? this.isGenerating,
currentOperation: currentOperation ?? this.currentOperation,
composePreview: composePreview ?? this.composePreview,
events: events ?? this.events,
nodeRenderStates: nodeRenderStates ?? this.nodeRenderStates,
renderQueue: renderQueue ?? this.renderQueue,
renderedNodeIds: renderedNodeIds ?? this.renderedNodeIds,
pendingNodes: pendingNodes ?? this.pendingNodes,
stickyWarning: stickyWarning ?? this.stickyWarning,
);
}
/// 获取当前选中的节点
SettingNode? get selectedNode {
if (selectedNodeId == null) return null;
return SettingNodeUtils.findNodeInTree(activeSession.rootNodes, selectedNodeId!);
}
/// 获取可以渲染的节点列表(父节点为空或已渲染)
List<String> get renderableNodeIds {
return SettingNodeUtils.getRenderableNodeIds(
activeSession.rootNodes,
renderQueue,
renderedNodeIds,
);
}
}
/// 生成完成
class SettingGenerationCompleted extends SettingGenerationState {
final List<StrategyTemplateInfo> strategies;
final List<SettingGenerationSession> sessions;
final String activeSessionId;
final SettingGenerationSession activeSession;
final String? selectedNodeId;
final String viewMode;
final String adjustmentPrompt;
final Map<String, SettingNode> pendingChanges;
final Set<String> highlightedNodeIds;
final Map<String, List<SettingNode>> editHistory;
final List<event_model.SettingGenerationEvent> events;
final String message;
// 新增的渲染状态管理字段
final Map<String, NodeRenderInfo> nodeRenderStates;
final Set<String> renderedNodeIds;
final String? stickyWarning;
const SettingGenerationCompleted({
required this.strategies,
required this.sessions,
required this.activeSessionId,
required this.activeSession,
this.selectedNodeId,
this.viewMode = 'compact',
this.adjustmentPrompt = '',
this.pendingChanges = const {},
this.highlightedNodeIds = const {},
this.editHistory = const {},
this.events = const [],
required this.message,
this.nodeRenderStates = const {},
this.renderedNodeIds = const {},
this.stickyWarning,
});
@override
List<Object?> get props => [
strategies,
sessions,
activeSessionId,
activeSession,
selectedNodeId,
viewMode,
adjustmentPrompt,
pendingChanges,
highlightedNodeIds,
editHistory,
events,
message,
nodeRenderStates,
renderedNodeIds,
stickyWarning,
];
SettingGenerationCompleted copyWith({
List<StrategyTemplateInfo>? strategies,
List<SettingGenerationSession>? sessions,
String? activeSessionId,
SettingGenerationSession? activeSession,
String? selectedNodeId,
String? viewMode,
String? adjustmentPrompt,
Map<String, SettingNode>? pendingChanges,
Set<String>? highlightedNodeIds,
Map<String, List<SettingNode>>? editHistory,
List<event_model.SettingGenerationEvent>? events,
String? message,
Map<String, NodeRenderInfo>? nodeRenderStates,
Set<String>? renderedNodeIds,
String? stickyWarning,
}) {
return SettingGenerationCompleted(
strategies: strategies ?? this.strategies,
sessions: sessions ?? this.sessions,
activeSessionId: activeSessionId ?? this.activeSessionId,
activeSession: activeSession ?? this.activeSession,
selectedNodeId: selectedNodeId ?? this.selectedNodeId,
viewMode: viewMode ?? this.viewMode,
adjustmentPrompt: adjustmentPrompt ?? this.adjustmentPrompt,
pendingChanges: pendingChanges ?? this.pendingChanges,
highlightedNodeIds: highlightedNodeIds ?? this.highlightedNodeIds,
editHistory: editHistory ?? this.editHistory,
events: events ?? this.events,
message: message ?? this.message,
nodeRenderStates: nodeRenderStates ?? this.nodeRenderStates,
renderedNodeIds: renderedNodeIds ?? this.renderedNodeIds,
stickyWarning: stickyWarning ?? this.stickyWarning,
);
}
/// 获取当前选中的节点
SettingNode? get selectedNode {
if (selectedNodeId == null) return null;
return SettingNodeUtils.findNodeInTree(activeSession.rootNodes, selectedNodeId!);
}
}
/// 节点修改中状态(专门用于节点修改,避免整个设定树重新渲染)
class SettingGenerationNodeUpdating extends SettingGenerationState {
final List<StrategyTemplateInfo> strategies;
final List<SettingGenerationSession> sessions;
final String activeSessionId;
final SettingGenerationSession activeSession;
final String? selectedNodeId;
final String viewMode;
final String adjustmentPrompt;
final Map<String, SettingNode> pendingChanges;
final Set<String> highlightedNodeIds;
final Map<String, List<SettingNode>> editHistory;
final List<event_model.SettingGenerationEvent> events;
final String message;
// 节点修改特有字段
final String updatingNodeId; // 正在修改的节点ID
final String modificationPrompt; // 修改提示词
final String scope; // 修改范围
final bool isUpdating; // 是否正在更新中
// 渲染状态管理字段
final Map<String, NodeRenderInfo> nodeRenderStates;
final Set<String> renderedNodeIds;
const SettingGenerationNodeUpdating({
required this.strategies,
required this.sessions,
required this.activeSessionId,
required this.activeSession,
this.selectedNodeId,
this.viewMode = 'compact',
this.adjustmentPrompt = '',
this.pendingChanges = const {},
this.highlightedNodeIds = const {},
this.editHistory = const {},
this.events = const [],
this.message = '',
required this.updatingNodeId,
this.modificationPrompt = '',
this.scope = 'self',
this.isUpdating = false,
this.nodeRenderStates = const {},
this.renderedNodeIds = const {},
});
@override
List<Object?> get props => [
strategies,
sessions,
activeSessionId,
activeSession,
selectedNodeId,
viewMode,
adjustmentPrompt,
pendingChanges,
highlightedNodeIds,
editHistory,
events,
message,
updatingNodeId,
modificationPrompt,
scope,
isUpdating,
nodeRenderStates,
renderedNodeIds,
];
SettingGenerationNodeUpdating copyWith({
List<StrategyTemplateInfo>? strategies,
List<SettingGenerationSession>? sessions,
String? activeSessionId,
SettingGenerationSession? activeSession,
String? selectedNodeId,
String? viewMode,
String? adjustmentPrompt,
Map<String, SettingNode>? pendingChanges,
Set<String>? highlightedNodeIds,
Map<String, List<SettingNode>>? editHistory,
List<event_model.SettingGenerationEvent>? events,
String? message,
String? updatingNodeId,
String? modificationPrompt,
String? scope,
bool? isUpdating,
Map<String, NodeRenderInfo>? nodeRenderStates,
Set<String>? renderedNodeIds,
}) {
return SettingGenerationNodeUpdating(
strategies: strategies ?? this.strategies,
sessions: sessions ?? this.sessions,
activeSessionId: activeSessionId ?? this.activeSessionId,
activeSession: activeSession ?? this.activeSession,
selectedNodeId: selectedNodeId ?? this.selectedNodeId,
viewMode: viewMode ?? this.viewMode,
adjustmentPrompt: adjustmentPrompt ?? this.adjustmentPrompt,
pendingChanges: pendingChanges ?? this.pendingChanges,
highlightedNodeIds: highlightedNodeIds ?? this.highlightedNodeIds,
editHistory: editHistory ?? this.editHistory,
events: events ?? this.events,
message: message ?? this.message,
updatingNodeId: updatingNodeId ?? this.updatingNodeId,
modificationPrompt: modificationPrompt ?? this.modificationPrompt,
scope: scope ?? this.scope,
isUpdating: isUpdating ?? this.isUpdating,
nodeRenderStates: nodeRenderStates ?? this.nodeRenderStates,
renderedNodeIds: renderedNodeIds ?? this.renderedNodeIds,
);
}
/// 获取当前选中的节点
SettingNode? get selectedNode {
if (selectedNodeId == null) return null;
return SettingNodeUtils.findNodeInTree(activeSession.rootNodes, selectedNodeId!);
}
/// 获取正在修改的节点
SettingNode? get updatingNode {
return SettingNodeUtils.findNodeInTree(activeSession.rootNodes, updatingNodeId);
}
}
/// 保存成功
class SettingGenerationSaved extends SettingGenerationState {
final List<String> savedSettingIds;
final String message;
// 新增保留会话列表和当前活跃会话ID避免UI刷新
final List<SettingGenerationSession> sessions;
final String? activeSessionId;
const SettingGenerationSaved({
required this.savedSettingIds,
this.message = '设定已成功保存',
this.sessions = const [],
this.activeSessionId,
});
@override
List<Object?> get props => [savedSettingIds, message, sessions, activeSessionId];
}
/// 错误状态
class SettingGenerationError extends SettingGenerationState {
final String message;
final dynamic error;
final StackTrace? stackTrace;
final bool isRecoverable;
// 新增:保留会话列表和当前活跃会话 ID避免 UI 在错误时丢失历史记录
final List<SettingGenerationSession> sessions;
final String? activeSessionId;
const SettingGenerationError({
required this.message,
this.error,
this.stackTrace,
this.isRecoverable = true,
this.sessions = const [],
this.activeSessionId,
});
@override
List<Object?> get props => [
message,
error,
stackTrace,
isRecoverable,
sessions,
activeSessionId,
];
SettingGenerationError copyWith({
String? message,
dynamic error,
StackTrace? stackTrace,
bool? isRecoverable,
List<SettingGenerationSession>? sessions,
String? activeSessionId,
}) {
return SettingGenerationError(
message: message ?? this.message,
error: error ?? this.error,
stackTrace: stackTrace ?? this.stackTrace,
isRecoverable: isRecoverable ?? this.isRecoverable,
sessions: sessions ?? this.sessions,
activeSessionId: activeSessionId ?? this.activeSessionId,
);
}
}

View File

@@ -0,0 +1,56 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:ainoval/models/novel_structure.dart'; // Novel 模型
import 'package:ainoval/services/api_service/repositories/editor_repository.dart'; // 引入 Repository
import 'package:ainoval/utils/logger.dart';
part 'sidebar_event.dart';
part 'sidebar_state.dart';
class SidebarBloc extends Bloc<SidebarEvent, SidebarState> {
final EditorRepository _editorRepository; // 依赖注入 EditorRepository
SidebarBloc({required EditorRepository editorRepository})
: _editorRepository = editorRepository,
super(SidebarInitial()) {
on<LoadNovelStructure>(_onLoadNovelStructure);
}
Future<void> _onLoadNovelStructure(
LoadNovelStructure event, Emitter<SidebarState> emit) async {
emit(SidebarLoading());
try {
AppLogger.i('SidebarBloc', '开始加载小说结构和场景摘要: ${event.novelId}');
// 使用专门的API获取包含场景摘要的小说结构
final novelWithSummaries = await _editorRepository.getNovelWithSceneSummaries(event.novelId, readOnly: true);
if (novelWithSummaries != null) {
AppLogger.i('SidebarBloc', '成功加载小说结构和场景摘要');
// 记录每个章节的摘要信息,用于调试
int chaptersWithScene = 0;
int totalScenes = 0;
for (final act in novelWithSummaries.acts) {
for (final chapter in act.chapters) {
if (chapter.scenes.isNotEmpty) {
chaptersWithScene++;
totalScenes += chapter.scenes.length;
}
}
}
AppLogger.i('SidebarBloc', '小说结构信息: 共${novelWithSummaries.acts.length}卷, '
'${chaptersWithScene}章含有场景, 总计${totalScenes}个场景');
emit(SidebarLoaded(novelStructure: novelWithSummaries));
} else {
AppLogger.e('SidebarBloc', '加载小说结构和场景摘要失败: 返回null');
emit(const SidebarError(message: '无法加载小说结构'));
}
} catch (e) {
AppLogger.e('SidebarBloc', '加载小说结构和场景摘要失败', e);
emit(SidebarError(message: '加载小说结构失败: ${e.toString()}'));
}
}
}

View File

@@ -0,0 +1,20 @@
part of 'sidebar_bloc.dart';
abstract class SidebarEvent extends Equatable {
const SidebarEvent();
@override
List<Object> get props => [];
}
// 加载小说结构和摘要事件
class LoadNovelStructure extends SidebarEvent {
final String novelId;
const LoadNovelStructure(this.novelId);
@override
List<Object> get props => [novelId];
}

View File

@@ -0,0 +1,30 @@
part of 'sidebar_bloc.dart';
abstract class SidebarState extends Equatable {
const SidebarState();
@override
List<Object?> get props => [];
}
class SidebarInitial extends SidebarState {}
class SidebarLoading extends SidebarState {}
class SidebarLoaded extends SidebarState {
final Novel novelStructure; // 包含完整结构和场景摘要的小说对象
const SidebarLoaded({required this.novelStructure});
@override
List<Object?> get props => [novelStructure];
}
class SidebarError extends SidebarState {
final String message;
const SidebarError({required this.message});
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,99 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import '../../services/api_service/repositories/impl/subscription_repository_impl.dart';
import '../../models/admin/subscription_models.dart';
part 'subscription_event.dart';
part 'subscription_state.dart';
class SubscriptionBloc extends Bloc<SubscriptionEvent, SubscriptionState> {
final SubscriptionRepositoryImpl subscriptionRepository;
SubscriptionBloc(this.subscriptionRepository) : super(SubscriptionInitial()) {
on<LoadSubscriptionPlans>(_onLoadSubscriptionPlans);
on<LoadSubscriptionStatistics>(_onLoadSubscriptionStatistics);
on<CreateSubscriptionPlan>(_onCreateSubscriptionPlan);
on<UpdateSubscriptionPlan>(_onUpdateSubscriptionPlan);
on<DeleteSubscriptionPlan>(_onDeleteSubscriptionPlan);
on<ToggleSubscriptionPlanStatus>(_onToggleSubscriptionPlanStatus);
}
Future<void> _onLoadSubscriptionPlans(
LoadSubscriptionPlans event,
Emitter<SubscriptionState> emit,
) async {
emit(SubscriptionLoading());
try {
final plans = await subscriptionRepository.getAllPlans();
emit(SubscriptionPlansLoaded(plans));
} catch (e) {
emit(SubscriptionError(e.toString()));
}
}
Future<void> _onLoadSubscriptionStatistics(
LoadSubscriptionStatistics event,
Emitter<SubscriptionState> emit,
) async {
emit(SubscriptionLoading());
try {
final statistics = await subscriptionRepository.getSubscriptionStatistics();
emit(SubscriptionStatisticsLoaded(statistics));
} catch (e) {
emit(SubscriptionError(e.toString()));
}
}
Future<void> _onCreateSubscriptionPlan(
CreateSubscriptionPlan event,
Emitter<SubscriptionState> emit,
) async {
try {
await subscriptionRepository.createPlan(event.plan);
// 重新加载订阅计划列表
add(LoadSubscriptionPlans());
} catch (e) {
emit(SubscriptionError(e.toString()));
}
}
Future<void> _onUpdateSubscriptionPlan(
UpdateSubscriptionPlan event,
Emitter<SubscriptionState> emit,
) async {
try {
await subscriptionRepository.updatePlan(event.planId, event.plan);
// 重新加载订阅计划列表
add(LoadSubscriptionPlans());
} catch (e) {
emit(SubscriptionError(e.toString()));
}
}
Future<void> _onDeleteSubscriptionPlan(
DeleteSubscriptionPlan event,
Emitter<SubscriptionState> emit,
) async {
try {
await subscriptionRepository.deletePlan(event.planId);
// 重新加载订阅计划列表
add(LoadSubscriptionPlans());
} catch (e) {
emit(SubscriptionError(e.toString()));
}
}
Future<void> _onToggleSubscriptionPlanStatus(
ToggleSubscriptionPlanStatus event,
Emitter<SubscriptionState> emit,
) async {
try {
await subscriptionRepository.togglePlanStatus(event.planId, event.active);
// 重新加载订阅计划列表
add(LoadSubscriptionPlans());
} catch (e) {
emit(SubscriptionError(e.toString()));
}
}
}

View File

@@ -0,0 +1,56 @@
part of 'subscription_bloc.dart';
abstract class SubscriptionEvent extends Equatable {
const SubscriptionEvent();
@override
List<Object?> get props => [];
}
class LoadSubscriptionPlans extends SubscriptionEvent {}
class LoadSubscriptionStatistics extends SubscriptionEvent {}
class CreateSubscriptionPlan extends SubscriptionEvent {
final SubscriptionPlan plan;
const CreateSubscriptionPlan(this.plan);
@override
List<Object> get props => [plan];
}
class UpdateSubscriptionPlan extends SubscriptionEvent {
final String planId;
final SubscriptionPlan plan;
const UpdateSubscriptionPlan({
required this.planId,
required this.plan,
});
@override
List<Object> get props => [planId, plan];
}
class DeleteSubscriptionPlan extends SubscriptionEvent {
final String planId;
const DeleteSubscriptionPlan(this.planId);
@override
List<Object> get props => [planId];
}
class ToggleSubscriptionPlanStatus extends SubscriptionEvent {
final String planId;
final bool active;
const ToggleSubscriptionPlanStatus({
required this.planId,
required this.active,
});
@override
List<Object> get props => [planId, active];
}

View File

@@ -0,0 +1,39 @@
part of 'subscription_bloc.dart';
abstract class SubscriptionState extends Equatable {
const SubscriptionState();
@override
List<Object?> get props => [];
}
class SubscriptionInitial extends SubscriptionState {}
class SubscriptionLoading extends SubscriptionState {}
class SubscriptionError extends SubscriptionState {
final String message;
const SubscriptionError(this.message);
@override
List<Object> get props => [message];
}
class SubscriptionPlansLoaded extends SubscriptionState {
final List<SubscriptionPlan> plans;
const SubscriptionPlansLoaded(this.plans);
@override
List<Object> get props => [plans];
}
class SubscriptionStatisticsLoaded extends SubscriptionState {
final SubscriptionStatistics statistics;
const SubscriptionStatisticsLoaded(this.statistics);
@override
List<Object> get props => [statistics];
}

View File

@@ -0,0 +1,98 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'theme_event.dart';
import 'theme_state.dart';
class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
static const String themeKey = 'theme_mode';
ThemeBloc() : super(const ThemeState(themeMode: ThemeMode.system)) {
on<ThemeInitialize>(_onThemeInitialize);
on<ThemeChanged>(_onThemeChanged);
on<ThemeToggled>(_onThemeToggled);
}
Future<void> _onThemeInitialize(
ThemeInitialize event,
Emitter<ThemeState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
final prefs = await SharedPreferences.getInstance();
final themeModeString = prefs.getString(themeKey);
ThemeMode themeMode = ThemeMode.system;
if (themeModeString != null) {
switch (themeModeString) {
case 'light':
themeMode = ThemeMode.light;
break;
case 'dark':
themeMode = ThemeMode.dark;
break;
case 'system':
themeMode = ThemeMode.system;
break;
}
}
emit(state.copyWith(
themeMode: themeMode,
isLoading: false,
));
} catch (e) {
emit(state.copyWith(
themeMode: ThemeMode.system,
isLoading: false,
));
}
}
Future<void> _onThemeChanged(
ThemeChanged event,
Emitter<ThemeState> emit,
) async {
emit(state.copyWith(themeMode: event.themeMode));
try {
final prefs = await SharedPreferences.getInstance();
String themeModeString;
switch (event.themeMode) {
case ThemeMode.light:
themeModeString = 'light';
break;
case ThemeMode.dark:
themeModeString = 'dark';
break;
case ThemeMode.system:
themeModeString = 'system';
break;
}
await prefs.setString(themeKey, themeModeString);
} catch (e) {
// 静默处理存储错误
}
}
Future<void> _onThemeToggled(
ThemeToggled event,
Emitter<ThemeState> emit,
) async {
ThemeMode newThemeMode;
switch (state.themeMode) {
case ThemeMode.light:
newThemeMode = ThemeMode.dark;
break;
case ThemeMode.dark:
newThemeMode = ThemeMode.system;
break;
case ThemeMode.system:
newThemeMode = ThemeMode.light;
break;
}
add(ThemeChanged(newThemeMode));
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
abstract class ThemeEvent {}
class ThemeInitialize extends ThemeEvent {}
class ThemeChanged extends ThemeEvent {
final ThemeMode themeMode;
ThemeChanged(this.themeMode);
}
class ThemeToggled extends ThemeEvent {}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
class ThemeState {
final ThemeMode themeMode;
final bool isLoading;
const ThemeState({
required this.themeMode,
this.isLoading = false,
});
ThemeState copyWith({
ThemeMode? themeMode,
bool? isLoading,
}) {
return ThemeState(
themeMode: themeMode ?? this.themeMode,
isLoading: isLoading ?? this.isLoading,
);
}
bool get isDarkMode => themeMode == ThemeMode.dark;
bool get isLightMode => themeMode == ThemeMode.light;
bool get isSystemMode => themeMode == ThemeMode.system;
}

View File

@@ -0,0 +1,250 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:ainoval/services/api_service/repositories/universal_ai_repository.dart';
import 'package:ainoval/models/ai_request_models.dart';
import 'package:ainoval/utils/logger.dart';
import 'universal_ai_event.dart';
import 'universal_ai_state.dart';
/// 通用AI请求BLoC
class UniversalAIBloc extends Bloc<UniversalAIEvent, UniversalAIState> {
final UniversalAIRepository _repository;
StreamSubscription? _streamSubscription;
UniversalAIBloc({
required UniversalAIRepository repository,
}) : _repository = repository,
super(const UniversalAIInitial()) {
on<SendAIRequestEvent>(_onSendAIRequest);
on<SendAIStreamRequestEvent>(_onSendAIStreamRequest);
on<PreviewAIRequestEvent>(_onPreviewAIRequest);
on<EstimateCostEvent>(_onEstimateCost);
on<StopStreamRequestEvent>(_onStopStreamRequest);
on<ClearResponseEvent>(_onClearResponse);
on<ResetStateEvent>(_onResetState);
}
/// 处理发送AI请求事件非流式
Future<void> _onSendAIRequest(
SendAIRequestEvent event,
Emitter<UniversalAIState> emit,
) async {
try {
emit(const UniversalAILoading(message: '正在发送请求...'));
AppLogger.d('UniversalAIBloc', '发送非流式AI请求: ${event.request.requestType}');
final response = await _repository.sendRequest(event.request);
emit(UniversalAISuccess(response: response));
AppLogger.d('UniversalAIBloc', '非流式AI请求完成');
} catch (e, stackTrace) {
AppLogger.e('UniversalAIBloc', '发送AI请求失败', e, stackTrace);
emit(UniversalAIError(
message: '请求失败: ${e.toString()}',
details: stackTrace.toString(),
));
}
}
/// 处理发送流式AI请求事件
Future<void> _onSendAIStreamRequest(
SendAIStreamRequestEvent event,
Emitter<UniversalAIState> emit,
) async {
try {
// 先取消之前的流式请求
await _streamSubscription?.cancel();
emit(const UniversalAILoading(message: '正在连接AI服务...'));
AppLogger.d('UniversalAIBloc', '开始流式AI请求: ${event.request.requestType}');
StringBuffer buffer = StringBuffer();
int tokenCount = 0;
bool isStreamCompleted = false;
final stream = _repository.streamRequest(event.request);
// 🚀 使用 emit.forEach 确保在事件处理器内部处理完整个流
await emit.forEach<UniversalAIResponse>(
stream,
onData: (response) {
// 🚀 检查是否收到结束信号
if (response.finishReason != null) {
AppLogger.i('UniversalAIBloc', '收到流式生成结束信号: ${response.finishReason}');
isStreamCompleted = true;
// 🚀 立即返回成功状态,不再发送流式状态
return UniversalAISuccess(
response: UniversalAIResponse(
id: response.id,
requestType: event.request.requestType,
content: buffer.toString(),
finishReason: response.finishReason,
model: response.model,
createdAt: response.createdAt,
metadata: response.metadata,
),
isStreaming: false, // 标记为非流式状态
);
}
// 🚀 只有在未完成时才累积内容
if (!isStreamCompleted && response.content.isNotEmpty) {
buffer.write(response.content);
tokenCount += response.tokenUsage?.completionTokens ?? 1;
//AppLogger.v('UniversalAIBloc', '收到流式响应片段,长度: ${response.content.length}');
return UniversalAIStreaming(
partialResponse: buffer.toString(),
tokenCount: tokenCount,
);
}
// 🚀 如果已完成或内容为空,保持当前状态
return emit.isDone ? const UniversalAIInitial() : const UniversalAIStreaming(partialResponse: '');
},
onError: (error, stackTrace) {
AppLogger.e('UniversalAIBloc', '流式AI请求错误', error, stackTrace);
return UniversalAIError(
message: '流式请求失败: ${error.toString()}',
details: stackTrace.toString(),
);
},
);
// 🚀 如果流正常结束但没有收到结束信号,手动发出成功状态
if (!isStreamCompleted && !emit.isDone) {
AppLogger.d('UniversalAIBloc', '流式AI请求完成无结束信号');
emit(UniversalAISuccess(
response: UniversalAIResponse(
id: DateTime.now().millisecondsSinceEpoch.toString(),
requestType: event.request.requestType,
content: buffer.toString(),
finishReason: 'stop',
),
isStreaming: false,
));
}
} catch (e, stackTrace) {
AppLogger.e('UniversalAIBloc', '流式AI请求失败', e, stackTrace);
emit(UniversalAIError(
message: '流式请求失败: ${e.toString()}',
details: stackTrace.toString(),
));
}
}
/// 处理预览AI请求事件
Future<void> _onPreviewAIRequest(
PreviewAIRequestEvent event,
Emitter<UniversalAIState> emit,
) async {
try {
emit(const UniversalAILoading(message: '正在生成预览...'));
AppLogger.d('UniversalAIBloc', '预览AI请求: ${event.request.requestType}');
final previewResponse = await _repository.previewRequest(event.request);
emit(UniversalAIPreviewSuccess(
previewResponse: previewResponse,
request: event.request,
));
AppLogger.d('UniversalAIBloc', '预览生成完成');
} catch (e, stackTrace) {
AppLogger.e('UniversalAIBloc', '预览AI请求失败', e, stackTrace);
emit(UniversalAIError(
message: '预览失败: ${e.toString()}',
details: stackTrace.toString(),
));
}
}
/// 🚀 新增:处理积分预估事件
Future<void> _onEstimateCost(
EstimateCostEvent event,
Emitter<UniversalAIState> emit,
) async {
try {
emit(const UniversalAILoading(message: '正在预估积分成本...'));
AppLogger.d('UniversalAIBloc', '预估AI请求积分成本: ${event.request.requestType}');
final costEstimation = await _repository.estimateCost(event.request);
if (costEstimation.success) {
emit(UniversalAICostEstimationSuccess(
costEstimation: costEstimation,
request: event.request,
));
AppLogger.d('UniversalAIBloc', '积分预估完成: ${costEstimation.estimatedCost}积分');
} else {
emit(UniversalAIError(
message: costEstimation.errorMessage ?? '积分预估失败',
canRetry: true,
));
}
} catch (e, stackTrace) {
AppLogger.e('UniversalAIBloc', '积分预估失败', e, stackTrace);
emit(UniversalAIError(
message: '积分预估失败: ${e.toString()}',
details: stackTrace.toString(),
canRetry: true,
));
}
}
/// 处理停止流式请求事件
Future<void> _onStopStreamRequest(
StopStreamRequestEvent event,
Emitter<UniversalAIState> emit,
) async {
AppLogger.d('UniversalAIBloc', '停止流式请求');
await _streamSubscription?.cancel();
_streamSubscription = null;
// 保留当前的部分响应
String? partialResponse;
if (state is UniversalAIStreaming) {
partialResponse = (state as UniversalAIStreaming).partialResponse;
}
emit(UniversalAICancelled(partialResponse: partialResponse));
}
/// 处理清除响应事件
Future<void> _onClearResponse(
ClearResponseEvent event,
Emitter<UniversalAIState> emit,
) async {
AppLogger.d('UniversalAIBloc', '清除响应');
emit(const UniversalAIInitial());
}
/// 处理重置状态事件
Future<void> _onResetState(
ResetStateEvent event,
Emitter<UniversalAIState> emit,
) async {
AppLogger.d('UniversalAIBloc', '重置状态');
await _streamSubscription?.cancel();
_streamSubscription = null;
emit(const UniversalAIInitial());
}
@override
Future<void> close() {
_streamSubscription?.cancel();
return super.close();
}
}

View File

@@ -0,0 +1,65 @@
import 'package:ainoval/models/ai_request_models.dart';
import 'package:equatable/equatable.dart';
/// 通用AI请求事件基类
abstract class UniversalAIEvent extends Equatable {
const UniversalAIEvent();
@override
List<Object?> get props => [];
}
/// 发送AI请求事件非流式
class SendAIRequestEvent extends UniversalAIEvent {
const SendAIRequestEvent(this.request);
final UniversalAIRequest request;
@override
List<Object?> get props => [request];
}
/// 发送流式AI请求事件
class SendAIStreamRequestEvent extends UniversalAIEvent {
const SendAIStreamRequestEvent(this.request);
final UniversalAIRequest request;
@override
List<Object?> get props => [request];
}
/// 预览AI请求事件
class PreviewAIRequestEvent extends UniversalAIEvent {
const PreviewAIRequestEvent(this.request);
final UniversalAIRequest request;
@override
List<Object?> get props => [request];
}
/// 停止流式请求事件
class StopStreamRequestEvent extends UniversalAIEvent {
const StopStreamRequestEvent();
}
/// 清除响应事件
class ClearResponseEvent extends UniversalAIEvent {
const ClearResponseEvent();
}
/// 重置状态事件
class ResetStateEvent extends UniversalAIEvent {
const ResetStateEvent();
}
/// 🚀 新增:积分预估事件
class EstimateCostEvent extends UniversalAIEvent {
const EstimateCostEvent(this.request);
final UniversalAIRequest request;
@override
List<Object?> get props => [request];
}

View File

@@ -0,0 +1,113 @@
import 'package:ainoval/models/ai_request_models.dart';
import 'package:equatable/equatable.dart';
/// 通用AI请求状态基类
abstract class UniversalAIState extends Equatable {
const UniversalAIState();
@override
List<Object?> get props => [];
}
/// 初始状态
class UniversalAIInitial extends UniversalAIState {
const UniversalAIInitial();
}
/// 加载中状态
class UniversalAILoading extends UniversalAIState {
const UniversalAILoading({
this.progress,
this.message,
});
final double? progress;
final String? message;
@override
List<Object?> get props => [progress, message];
}
/// 流式响应进行中状态
class UniversalAIStreaming extends UniversalAIState {
const UniversalAIStreaming({
required this.partialResponse,
this.tokenCount = 0,
});
final String partialResponse;
final int tokenCount;
@override
List<Object?> get props => [partialResponse, tokenCount];
}
/// 请求成功状态
class UniversalAISuccess extends UniversalAIState {
const UniversalAISuccess({
required this.response,
this.isStreaming = false,
});
final UniversalAIResponse response;
final bool isStreaming;
@override
List<Object?> get props => [response, isStreaming];
}
/// 预览成功状态
class UniversalAIPreviewSuccess extends UniversalAIState {
const UniversalAIPreviewSuccess({
required this.previewResponse,
required this.request,
});
final UniversalAIPreviewResponse previewResponse;
final UniversalAIRequest request;
@override
List<Object?> get props => [previewResponse, request];
}
/// 错误状态
class UniversalAIError extends UniversalAIState {
const UniversalAIError({
required this.message,
this.details,
this.canRetry = true,
});
final String message;
final String? details;
final bool canRetry;
@override
List<Object?> get props => [message, details, canRetry];
}
/// 请求被取消状态
class UniversalAICancelled extends UniversalAIState {
const UniversalAICancelled({
this.partialResponse,
});
final String? partialResponse;
@override
List<Object?> get props => [partialResponse];
}
/// 🚀 新增:积分预估成功状态
class UniversalAICostEstimationSuccess extends UniversalAIState {
const UniversalAICostEstimationSuccess({
required this.costEstimation,
required this.request,
});
final CostEstimationResponse costEstimation;
final UniversalAIRequest request;
@override
List<Object?> get props => [costEstimation, request];
}