马良AI写作初始化仓库
This commit is contained in:
70
AINoval/lib/admin_main.dart
Normal file
70
AINoval/lib/admin_main.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import 'blocs/admin/admin_bloc.dart';
|
||||
import 'config/app_config.dart';
|
||||
import 'screens/admin/admin_login_screen.dart';
|
||||
import 'services/api_service/repositories/impl/admin_repository_impl.dart';
|
||||
import 'services/api_service/repositories/impl/admin/llm_observability_repository_impl.dart';
|
||||
import 'services/api_service/repositories/impl/subscription_repository_impl.dart';
|
||||
import 'services/api_service/repositories/impl/admin/billing_repository_impl.dart';
|
||||
import 'blocs/subscription/subscription_bloc.dart';
|
||||
import 'services/api_service/base/api_client.dart';
|
||||
import 'utils/app_theme.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// 设置为管理员模式
|
||||
AppConfig.setAdminMode(true);
|
||||
|
||||
// 初始化服务
|
||||
await _setupAdminServices();
|
||||
|
||||
runApp(const AdminApp());
|
||||
}
|
||||
|
||||
Future<void> _setupAdminServices() async {
|
||||
final getIt = GetIt.instance;
|
||||
|
||||
// 注册API客户端(如果还没有注册)
|
||||
if (!getIt.isRegistered<ApiClient>()) {
|
||||
getIt.registerLazySingleton<ApiClient>(() => ApiClient());
|
||||
}
|
||||
|
||||
// 注册管理员专用服务
|
||||
getIt.registerLazySingleton<AdminRepositoryImpl>(() => AdminRepositoryImpl());
|
||||
getIt.registerLazySingleton<LLMObservabilityRepositoryImpl>(() =>
|
||||
LLMObservabilityRepositoryImpl(apiClient: getIt<ApiClient>()));
|
||||
// 计费审计仓库
|
||||
getIt.registerLazySingleton<BillingRepositoryImpl>(() =>
|
||||
BillingRepositoryImpl(apiClient: getIt<ApiClient>()));
|
||||
// 订阅仓库与Bloc
|
||||
getIt.registerLazySingleton<SubscriptionRepositoryImpl>(() => SubscriptionRepositoryImpl(apiClient: getIt<ApiClient>()));
|
||||
getIt.registerFactory<SubscriptionBloc>(() => SubscriptionBloc(getIt<SubscriptionRepositoryImpl>()));
|
||||
|
||||
getIt.registerFactory<AdminBloc>(() => AdminBloc(getIt<AdminRepositoryImpl>()));
|
||||
}
|
||||
|
||||
class AdminApp extends StatelessWidget {
|
||||
const AdminApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (context) => GetIt.instance<AdminBloc>()),
|
||||
BlocProvider(create: (context) => GetIt.instance<SubscriptionBloc>()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'AI Novel Writer - Admin Dashboard',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
themeMode: ThemeMode.system,
|
||||
home: const AdminLoginScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
220
AINoval/lib/blocs/admin/admin_bloc.dart
Normal file
220
AINoval/lib/blocs/admin/admin_bloc.dart
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
153
AINoval/lib/blocs/admin/admin_event.dart
Normal file
153
AINoval/lib/blocs/admin/admin_event.dart
Normal 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];
|
||||
}
|
||||
66
AINoval/lib/blocs/admin/admin_state.dart
Normal file
66
AINoval/lib/blocs/admin/admin_state.dart
Normal 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];
|
||||
}
|
||||
746
AINoval/lib/blocs/ai_config/ai_config_bloc.dart
Normal file
746
AINoval/lib/blocs/ai_config/ai_config_bloc.dart
Normal 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', '模型加载完成,已缓存,触发GetProviderDefaultConfig,provider=${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', '已清除所有模型缓存');
|
||||
}
|
||||
}
|
||||
}
|
||||
189
AINoval/lib/blocs/ai_config/ai_config_event.dart
Normal file
189
AINoval/lib/blocs/ai_config/ai_config_event.dart
Normal 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();
|
||||
}
|
||||
161
AINoval/lib/blocs/ai_config/ai_config_state.dart
Normal file
161
AINoval/lib/blocs/ai_config/ai_config_state.dart
Normal 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,
|
||||
];
|
||||
}
|
||||
@@ -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,
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
529
AINoval/lib/blocs/auth/auth_bloc.dart
Normal file
529
AINoval/lib/blocs/auth/auth_bloc.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
1557
AINoval/lib/blocs/chat/chat_bloc.dart
Normal file
1557
AINoval/lib/blocs/chat/chat_bloc.dart
Normal file
File diff suppressed because it is too large
Load Diff
177
AINoval/lib/blocs/chat/chat_event.dart
Normal file
177
AINoval/lib/blocs/chat/chat_event.dart
Normal 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];
|
||||
}
|
||||
146
AINoval/lib/blocs/chat/chat_state.dart
Normal file
146
AINoval/lib/blocs/chat/chat_state.dart
Normal 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];
|
||||
}
|
||||
65
AINoval/lib/blocs/credit/credit_bloc.dart
Normal file
65
AINoval/lib/blocs/credit/credit_bloc.dart
Normal 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()}'));
|
||||
}
|
||||
}
|
||||
}
|
||||
24
AINoval/lib/blocs/credit/credit_event.dart
Normal file
24
AINoval/lib/blocs/credit/credit_event.dart
Normal 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();
|
||||
}
|
||||
48
AINoval/lib/blocs/credit/credit_state.dart
Normal file
48
AINoval/lib/blocs/credit/credit_state.dart
Normal 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];
|
||||
}
|
||||
3140
AINoval/lib/blocs/editor/editor_bloc.dart
Normal file
3140
AINoval/lib/blocs/editor/editor_bloc.dart
Normal file
File diff suppressed because it is too large
Load Diff
585
AINoval/lib/blocs/editor/editor_event.dart
Normal file
585
AINoval/lib/blocs/editor/editor_event.dart
Normal 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];
|
||||
}
|
||||
270
AINoval/lib/blocs/editor/editor_state.dart
Normal file
270
AINoval/lib/blocs/editor/editor_state.dart
Normal 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];
|
||||
}
|
||||
186
AINoval/lib/blocs/editor_version_bloc.dart
Normal file
186
AINoval/lib/blocs/editor_version_bloc.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
109
AINoval/lib/blocs/editor_version_event.dart
Normal file
109
AINoval/lib/blocs/editor_version_event.dart
Normal 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,
|
||||
];
|
||||
}
|
||||
68
AINoval/lib/blocs/editor_version_state.dart
Normal file
68
AINoval/lib/blocs/editor_version_state.dart
Normal 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];
|
||||
}
|
||||
656
AINoval/lib/blocs/next_outline/next_outline_bloc.dart
Normal file
656
AINoval/lib/blocs/next_outline/next_outline_bloc.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
133
AINoval/lib/blocs/next_outline/next_outline_event.dart
Normal file
133
AINoval/lib/blocs/next_outline/next_outline_event.dart
Normal 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];
|
||||
}
|
||||
240
AINoval/lib/blocs/next_outline/next_outline_state.dart
Normal file
240
AINoval/lib/blocs/next_outline/next_outline_state.dart
Normal 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,
|
||||
];
|
||||
}
|
||||
316
AINoval/lib/blocs/novel_import/novel_import_bloc.dart
Normal file
316
AINoval/lib/blocs/novel_import/novel_import_bloc.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
132
AINoval/lib/blocs/novel_import/novel_import_event.dart
Normal file
132
AINoval/lib/blocs/novel_import/novel_import_event.dart
Normal 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();
|
||||
}
|
||||
231
AINoval/lib/blocs/novel_import/novel_import_state.dart
Normal file
231
AINoval/lib/blocs/novel_import/novel_import_state.dart
Normal 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?,
|
||||
);
|
||||
}
|
||||
}
|
||||
400
AINoval/lib/blocs/novel_list/novel_list_bloc.dart
Normal file
400
AINoval/lib/blocs/novel_list/novel_list_bloc.dart
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
401
AINoval/lib/blocs/plan/plan_bloc.dart
Normal file
401
AINoval/lib/blocs/plan/plan_bloc.dart
Normal 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()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
138
AINoval/lib/blocs/plan/plan_event.dart
Normal file
138
AINoval/lib/blocs/plan/plan_event.dart
Normal 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];
|
||||
}
|
||||
61
AINoval/lib/blocs/plan/plan_state.dart
Normal file
61
AINoval/lib/blocs/plan/plan_state.dart
Normal 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];
|
||||
}
|
||||
996
AINoval/lib/blocs/preset/preset_bloc.dart
Normal file
996
AINoval/lib/blocs/preset/preset_bloc.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
272
AINoval/lib/blocs/preset/preset_event.dart
Normal file
272
AINoval/lib/blocs/preset/preset_event.dart
Normal 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];
|
||||
}
|
||||
240
AINoval/lib/blocs/preset/preset_state.dart
Normal file
240
AINoval/lib/blocs/preset/preset_state.dart
Normal 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',
|
||||
)''';
|
||||
}
|
||||
}
|
||||
632
AINoval/lib/blocs/prompt_new/prompt_new_bloc.dart
Normal file
632
AINoval/lib/blocs/prompt_new/prompt_new_bloc.dart
Normal 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()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
134
AINoval/lib/blocs/prompt_new/prompt_new_event.dart
Normal file
134
AINoval/lib/blocs/prompt_new/prompt_new_event.dart
Normal 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();
|
||||
}
|
||||
242
AINoval/lib/blocs/prompt_new/prompt_new_state.dart
Normal file
242
AINoval/lib/blocs/prompt_new/prompt_new_state.dart
Normal 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,
|
||||
];
|
||||
}
|
||||
62
AINoval/lib/blocs/public_models/public_models_bloc.dart
Normal file
62
AINoval/lib/blocs/public_models/public_models_bloc.dart
Normal 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()}'));
|
||||
}
|
||||
}
|
||||
}
|
||||
19
AINoval/lib/blocs/public_models/public_models_event.dart
Normal file
19
AINoval/lib/blocs/public_models/public_models_event.dart
Normal 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();
|
||||
}
|
||||
48
AINoval/lib/blocs/public_models/public_models_state.dart
Normal file
48
AINoval/lib/blocs/public_models/public_models_state.dart
Normal 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];
|
||||
}
|
||||
905
AINoval/lib/blocs/setting/setting_bloc.dart
Normal file
905
AINoval/lib/blocs/setting/setting_bloc.dart
Normal 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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
3457
AINoval/lib/blocs/setting_generation/setting_generation_bloc.dart
Normal file
3457
AINoval/lib/blocs/setting_generation/setting_generation_bloc.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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];
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
56
AINoval/lib/blocs/sidebar/sidebar_bloc.dart
Normal file
56
AINoval/lib/blocs/sidebar/sidebar_bloc.dart
Normal 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()}'));
|
||||
}
|
||||
}
|
||||
}
|
||||
20
AINoval/lib/blocs/sidebar/sidebar_event.dart
Normal file
20
AINoval/lib/blocs/sidebar/sidebar_event.dart
Normal 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];
|
||||
}
|
||||
30
AINoval/lib/blocs/sidebar/sidebar_state.dart
Normal file
30
AINoval/lib/blocs/sidebar/sidebar_state.dart
Normal 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];
|
||||
}
|
||||
99
AINoval/lib/blocs/subscription/subscription_bloc.dart
Normal file
99
AINoval/lib/blocs/subscription/subscription_bloc.dart
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
56
AINoval/lib/blocs/subscription/subscription_event.dart
Normal file
56
AINoval/lib/blocs/subscription/subscription_event.dart
Normal 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];
|
||||
}
|
||||
39
AINoval/lib/blocs/subscription/subscription_state.dart
Normal file
39
AINoval/lib/blocs/subscription/subscription_state.dart
Normal 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];
|
||||
}
|
||||
98
AINoval/lib/blocs/theme/theme_bloc.dart
Normal file
98
AINoval/lib/blocs/theme/theme_bloc.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
13
AINoval/lib/blocs/theme/theme_event.dart
Normal file
13
AINoval/lib/blocs/theme/theme_event.dart
Normal 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 {}
|
||||
25
AINoval/lib/blocs/theme/theme_state.dart
Normal file
25
AINoval/lib/blocs/theme/theme_state.dart
Normal 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;
|
||||
}
|
||||
250
AINoval/lib/blocs/universal_ai/universal_ai_bloc.dart
Normal file
250
AINoval/lib/blocs/universal_ai/universal_ai_bloc.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
65
AINoval/lib/blocs/universal_ai/universal_ai_event.dart
Normal file
65
AINoval/lib/blocs/universal_ai/universal_ai_event.dart
Normal 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];
|
||||
}
|
||||
113
AINoval/lib/blocs/universal_ai/universal_ai_state.dart
Normal file
113
AINoval/lib/blocs/universal_ai/universal_ai_state.dart
Normal 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];
|
||||
}
|
||||
127
AINoval/lib/components/editable_title.dart
Normal file
127
AINoval/lib/components/editable_title.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Debouncer {
|
||||
|
||||
Debouncer({this.delay = const Duration(milliseconds: 500)});
|
||||
Timer? _timer;
|
||||
final Duration delay;
|
||||
|
||||
void run(Function() action) {
|
||||
_timer?.cancel();
|
||||
_timer = Timer(delay, action);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
class EditableTitle extends StatefulWidget {
|
||||
|
||||
const EditableTitle({
|
||||
Key? key,
|
||||
required this.initialText,
|
||||
this.onChanged,
|
||||
this.onSubmitted,
|
||||
this.commitOnBlur = true,
|
||||
this.style,
|
||||
this.textAlign = TextAlign.left,
|
||||
this.autofocus = false,
|
||||
}) : super(key: key);
|
||||
final String initialText;
|
||||
// 可选:仅用于本地UI联动(不做持久化)
|
||||
final Function(String)? onChanged;
|
||||
// 提交时回调:回车或失焦触发
|
||||
final Function(String)? onSubmitted;
|
||||
// 失焦时是否提交
|
||||
final bool commitOnBlur;
|
||||
final TextStyle? style;
|
||||
final TextAlign textAlign;
|
||||
final bool autofocus;
|
||||
|
||||
@override
|
||||
State<EditableTitle> createState() => _EditableTitleState();
|
||||
}
|
||||
|
||||
class _EditableTitleState extends State<EditableTitle> {
|
||||
late TextEditingController _controller;
|
||||
late Debouncer _debouncer;
|
||||
late FocusNode _focusNode;
|
||||
String _lastCommittedText = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.initialText);
|
||||
_debouncer = Debouncer();
|
||||
_focusNode = FocusNode();
|
||||
_lastCommittedText = widget.initialText;
|
||||
|
||||
_focusNode.addListener(() {
|
||||
if (!_focusNode.hasFocus && widget.commitOnBlur) {
|
||||
_commitIfChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(EditableTitle oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.initialText != widget.initialText) {
|
||||
_controller.text = widget.initialText;
|
||||
// 外部更新时同步已提交文本基线
|
||||
_lastCommittedText = widget.initialText;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debouncer.dispose();
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _commitIfChanged() {
|
||||
final current = _controller.text;
|
||||
if (current != _lastCommittedText) {
|
||||
_lastCommittedText = current;
|
||||
if (widget.onSubmitted != null) {
|
||||
widget.onSubmitted!(current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
style: widget.style,
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true,
|
||||
),
|
||||
textAlign: widget.textAlign,
|
||||
autofocus: widget.autofocus,
|
||||
// onChanged 仅用于本地UI联动(不持久化)
|
||||
onChanged: (value) {
|
||||
if (widget.onChanged != null) {
|
||||
_debouncer.run(() {
|
||||
widget.onChanged!(value);
|
||||
});
|
||||
}
|
||||
},
|
||||
// 按下回车时提交
|
||||
onSubmitted: (_) {
|
||||
_commitIfChanged();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
165
AINoval/lib/config/app_config.dart
Normal file
165
AINoval/lib/config/app_config.dart
Normal file
@@ -0,0 +1,165 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
// 条件导入:在非Web平台导入dart:io,在Web平台导入dart:html
|
||||
|
||||
/// 应用环境枚举
|
||||
enum Environment {
|
||||
development,
|
||||
production,
|
||||
}
|
||||
|
||||
/// 应用配置类
|
||||
///
|
||||
/// 用于管理应用的环境配置和模拟数据设置
|
||||
class AppConfig {
|
||||
/// 私有构造函数,防止实例化
|
||||
AppConfig._();
|
||||
|
||||
/// 当前环境
|
||||
static Environment _environment = kDebugMode ? Environment.development : Environment.production;
|
||||
|
||||
/// 是否强制使用模拟数据(无论环境如何)
|
||||
static bool _forceMockData = false;
|
||||
|
||||
/// 是否为管理员模式
|
||||
static bool _isAdminMode = false;
|
||||
|
||||
/// 获取当前环境
|
||||
static Environment get environment => _environment;
|
||||
|
||||
/// 设置当前环境
|
||||
static void setEnvironment(Environment env) {
|
||||
_environment = env;
|
||||
}
|
||||
|
||||
/// 是否应该使用模拟数据
|
||||
static bool get shouldUseMockData => _forceMockData;
|
||||
|
||||
/// 强制使用/不使用模拟数据
|
||||
static void setUseMockData(bool useMock) {
|
||||
_forceMockData = useMock;
|
||||
}
|
||||
|
||||
/// 获取是否为管理员模式
|
||||
static bool get isAdminMode => _isAdminMode;
|
||||
|
||||
/// 设置管理员模式
|
||||
static void setAdminMode(bool isAdmin) {
|
||||
_isAdminMode = isAdmin;
|
||||
}
|
||||
|
||||
/// 检查是否为Android平台(仅在非Web平台有效)
|
||||
static bool get _isAndroid {
|
||||
if (kIsWeb) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// 只有在非Web平台才能访问Platform
|
||||
return Platform.isAndroid;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// API基础URL
|
||||
static String get apiBaseUrl {
|
||||
switch (_environment) {
|
||||
case Environment.development:
|
||||
// 在Web平台上,直接使用localhost
|
||||
if (kIsWeb) {
|
||||
return 'http://127.0.0.1:18080/api/v1';
|
||||
}
|
||||
// 在Android平台上,使用10.0.2.2来访问宿主机
|
||||
// 在其他平台上使用127.0.0.1
|
||||
else if (_isAndroid) {
|
||||
return 'http://10.0.2.2:18080/api/v1';
|
||||
} else {
|
||||
return 'http://127.0.0.1:18080/api/v1';
|
||||
}
|
||||
case Environment.production:
|
||||
return '/api/v1';
|
||||
}
|
||||
}
|
||||
|
||||
/// API认证令牌
|
||||
static String? _authToken;
|
||||
|
||||
/// 设置认证令牌
|
||||
static void setAuthToken(String? token) {
|
||||
_authToken = token;
|
||||
}
|
||||
|
||||
/// 获取认证令牌
|
||||
static String? get authToken => _authToken;
|
||||
|
||||
/// 当前用户ID
|
||||
static String? _userId;
|
||||
|
||||
/// 设置当前用户ID
|
||||
static void setUserId(String? id) {
|
||||
_userId = id;
|
||||
}
|
||||
|
||||
/// 获取当前用户ID
|
||||
static String? get userId => _userId;
|
||||
|
||||
/// 当前用户名
|
||||
static String? _username;
|
||||
|
||||
/// 设置当前用户名
|
||||
static void setUsername(String? name) {
|
||||
_username = name;
|
||||
}
|
||||
|
||||
/// 获取当前用户名
|
||||
static String? get username => _username;
|
||||
|
||||
/// 日志级别
|
||||
static LogLevel get logLevel {
|
||||
switch (_environment) {
|
||||
case Environment.development:
|
||||
return LogLevel.debug;
|
||||
case Environment.production:
|
||||
return LogLevel.error;
|
||||
}
|
||||
}
|
||||
|
||||
// 当前编辑/阅读的小说ID
|
||||
static String? currentNovelId;
|
||||
|
||||
// 应用版本信息
|
||||
static String appVersion = '1.0.0';
|
||||
static bool isDebugMode = kDebugMode;
|
||||
|
||||
// 初始化配置
|
||||
static Future<void> initialize() async {
|
||||
// 这里可以从本地存储或其他来源加载配置
|
||||
}
|
||||
|
||||
// 保存用户状态
|
||||
static Future<void> saveUserState() async {
|
||||
// 将用户状态保存到本地存储
|
||||
}
|
||||
|
||||
// 清除用户状态
|
||||
static Future<void> clearUserState() async {
|
||||
_userId = null;
|
||||
_username = null;
|
||||
_authToken = null;
|
||||
}
|
||||
|
||||
// 设置当前小说
|
||||
static void setCurrentNovel(String? id) {
|
||||
currentNovelId = id;
|
||||
}
|
||||
}
|
||||
|
||||
/// 日志级别枚举
|
||||
enum LogLevel {
|
||||
debug, // 调试信息
|
||||
info, // 一般信息
|
||||
warning, // 警告信息
|
||||
error, // 错误信息
|
||||
}
|
||||
463
AINoval/lib/config/provider_icons.dart
Normal file
463
AINoval/lib/config/provider_icons.dart
Normal file
@@ -0,0 +1,463 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
/// 图标尺寸枚举
|
||||
enum IconSize {
|
||||
small, // 16px
|
||||
medium, // 24px
|
||||
large, // 32px
|
||||
extraLarge, // 48px
|
||||
}
|
||||
|
||||
/// AI模型提供商图标管理类
|
||||
/// 提供统一的图标获取接口
|
||||
class ProviderIcons {
|
||||
// 私有构造函数,防止实例化
|
||||
ProviderIcons._();
|
||||
|
||||
/// 提供商图标路径映射表
|
||||
static const Map<String, String> _providerIconPaths = {
|
||||
// OpenAI系列
|
||||
'openai': 'assets/icons/openai.svg',
|
||||
'chatgpt': 'assets/icons/openai.svg',
|
||||
'gpt': 'assets/icons/openai.svg',
|
||||
|
||||
// Anthropic系列
|
||||
'anthropic': 'assets/icons/anthropic.svg',
|
||||
'claude': 'assets/icons/claude-color.svg',
|
||||
|
||||
// Google系列
|
||||
'google': 'assets/icons/gemini-color.svg',
|
||||
'gemini': 'assets/icons/gemini-color.svg',
|
||||
'bard': 'assets/icons/gemini-color.svg',
|
||||
|
||||
// 微软系列
|
||||
'microsoft': 'assets/icons/microsoft-color.svg',
|
||||
'azure': 'assets/icons/microsoft-color.svg',
|
||||
'copilot': 'assets/icons/microsoft-color.svg',
|
||||
|
||||
// Meta系列
|
||||
'meta': 'assets/icons/meta-color.svg',
|
||||
'llama': 'assets/icons/meta-color.svg',
|
||||
'facebook': 'assets/icons/meta-color.svg',
|
||||
|
||||
// 字节跳动系列
|
||||
'bytedance': 'assets/icons/bytedance-color.svg',
|
||||
'doubao': 'assets/icons/doubao-color.svg',
|
||||
'豆包': 'assets/icons/doubao-color.svg',
|
||||
|
||||
// 智谱AI系列
|
||||
'zhipu': 'assets/icons/zhipu-color.svg',
|
||||
'glm': 'assets/icons/zhipu-color.svg',
|
||||
'智谱': 'assets/icons/zhipu-color.svg',
|
||||
|
||||
// 阿里系列
|
||||
'qwen': 'assets/icons/qwen-color.svg',
|
||||
'tongyi': 'assets/icons/qwen-color.svg',
|
||||
'alibaba': 'assets/icons/qwen-color.svg',
|
||||
'通义': 'assets/icons/qwen-color.svg',
|
||||
|
||||
// DeepSeek系列
|
||||
'deepseek': 'assets/icons/deepseek-color.svg',
|
||||
|
||||
// Mistral系列
|
||||
'mistral': 'assets/icons/mistral-color.svg',
|
||||
|
||||
// 硅基流动
|
||||
'siliconcloud': 'assets/icons/siliconcloud-color.svg',
|
||||
'siliconflow': 'assets/icons/siliconcloud-color.svg',
|
||||
|
||||
// Perplexity
|
||||
'perplexity': 'assets/icons/perplexity-color.svg',
|
||||
|
||||
// HuggingFace
|
||||
'huggingface': 'assets/icons/huggingface-color.svg',
|
||||
'hf': 'assets/icons/huggingface-color.svg',
|
||||
|
||||
// Stability AI
|
||||
'stability': 'assets/icons/stability-color.svg',
|
||||
'stable-diffusion': 'assets/icons/stability-color.svg',
|
||||
|
||||
// OpenRouter
|
||||
'openrouter': 'assets/icons/openrouter.svg',
|
||||
|
||||
// Ollama
|
||||
'ollama': 'assets/icons/ollama.svg',
|
||||
|
||||
// xAI Grok
|
||||
'xai': 'assets/icons/grok.svg',
|
||||
'grok': 'assets/icons/grok.svg',
|
||||
// xAI Grok
|
||||
'x-ai': 'assets/icons/grok.svg',
|
||||
'X-ai': 'assets/icons/grok.svg',
|
||||
|
||||
|
||||
// Midjourney
|
||||
'midjourney': 'assets/icons/midjourney.svg',
|
||||
'mj': 'assets/icons/midjourney.svg',
|
||||
|
||||
// LM Studio
|
||||
'lm-studio': 'assets/icons/ollama.svg',
|
||||
'lmstudio': 'assets/icons/ollama.svg',
|
||||
|
||||
// LocalAI
|
||||
'localai': 'assets/icons/ollama.svg',
|
||||
'local': 'assets/icons/ollama.svg',
|
||||
};
|
||||
|
||||
/// 提供商默认颜色映射
|
||||
/// 颜色配置参考: https://lobehub.com/zh/icons
|
||||
static const Map<String, Color> _providerColors = {
|
||||
// OpenAI系列 - #000
|
||||
'openai': Color(0xFF000000),
|
||||
'chatgpt': Color(0xFF000000),
|
||||
'gpt': Color(0xFF000000),
|
||||
|
||||
// Anthropic系列 - #F1F0E8 (浅色背景) / Claude: #D97757
|
||||
'anthropic': Color(0xFFF1F0E8),
|
||||
'claude': Color(0xFFD97757),
|
||||
|
||||
// Google系列 - #1C69FF (Gemini) / #FFF (Google)
|
||||
'google': Color(0xFFFFFFFF),
|
||||
'gemini': Color(0xFF1C69FF),
|
||||
'bard': Color(0xFF1C69FF),
|
||||
|
||||
// 微软系列 - #00A4EF / Copilot: #FFF
|
||||
'microsoft': Color(0xFF00A4EF),
|
||||
'azure': Color(0xFF00A4EF),
|
||||
'copilot': Color(0xFFFFFFFF),
|
||||
|
||||
// Meta系列 - #1D65C1
|
||||
'meta': Color(0xFF1D65C1),
|
||||
'llama': Color(0xFF1D65C1),
|
||||
'facebook': Color(0xFF1D65C1),
|
||||
|
||||
// 字节跳动系列 - #325AB4 / Doubao: #FFF
|
||||
'bytedance': Color(0xFF325AB4),
|
||||
'doubao': Color(0xFFFFFFFF),
|
||||
'豆包': Color(0xFFFFFFFF),
|
||||
|
||||
// 智谱AI系列 - #3859FF / ChatGLM: #4268FA
|
||||
'zhipu': Color(0xFF3859FF),
|
||||
'glm': Color(0xFF4268FA),
|
||||
'智谱': Color(0xFF3859FF),
|
||||
|
||||
// 阿里系列 - #615CED
|
||||
'qwen': Color(0xFF615CED),
|
||||
'tongyi': Color(0xFF615CED),
|
||||
'alibaba': Color(0xFF615CED),
|
||||
'通义': Color(0xFF615CED),
|
||||
|
||||
// DeepSeek系列
|
||||
'deepseek': Color(0xFF4D6BFE),
|
||||
|
||||
// Mistral系列 - #FA520F
|
||||
'mistral': Color(0xFFFA520F),
|
||||
|
||||
// 硅基流动
|
||||
'siliconcloud': Color(0xFF7C3AED),
|
||||
'siliconflow': Color(0xFF7C3AED),
|
||||
|
||||
// Perplexity - #22B8CD
|
||||
'perplexity': Color(0xFF22B8CD),
|
||||
|
||||
// HuggingFace - #FFF
|
||||
'huggingface': Color(0xFFFFFFFF),
|
||||
'hf': Color(0xFFFFFFFF),
|
||||
|
||||
// Stability AI - #330066
|
||||
'stability': Color(0xFF330066),
|
||||
'stable-diffusion': Color(0xFF330066),
|
||||
|
||||
// OpenRouter - #6566F1
|
||||
'openrouter': Color(0xFF6566F1),
|
||||
|
||||
// Ollama - #FFF
|
||||
'ollama': Color(0xFFFFFFFF),
|
||||
|
||||
// xAI Grok - #000
|
||||
'xai': Color(0xFF000000),
|
||||
'grok': Color(0xFF000000),
|
||||
'x-ai': Color(0xFF000000),
|
||||
'X-ai': Color(0xFF000000),
|
||||
|
||||
// Midjourney - #FFF
|
||||
'midjourney': Color(0xFFFFFFFF),
|
||||
'mj': Color(0xFFFFFFFF),
|
||||
|
||||
// Groq - #F55036
|
||||
'groq': Color(0xFFF55036),
|
||||
|
||||
// together.ai - #0F6FFF
|
||||
'together': Color(0xFF0F6FFF),
|
||||
'together.ai': Color(0xFF0F6FFF),
|
||||
|
||||
// Fireworks - #5019C5
|
||||
'fireworks': Color(0xFF5019C5),
|
||||
|
||||
// Cohere - #39594D
|
||||
'cohere': Color(0xFF39594D),
|
||||
|
||||
// Replicate - #EA2805
|
||||
'replicate': Color(0xFFEA2805),
|
||||
|
||||
// LM Studio / LocalAI
|
||||
'lm-studio': Color(0xFFFFFFFF),
|
||||
'lmstudio': Color(0xFFFFFFFF),
|
||||
'localai': Color(0xFFFFFFFF),
|
||||
'local': Color(0xFFFFFFFF),
|
||||
};
|
||||
|
||||
/// 获取提供商图标(优化版本)
|
||||
///
|
||||
/// [provider] 提供商名称,大小写不敏感
|
||||
/// [size] 图标大小,默认为24(提高默认尺寸提升清晰度)
|
||||
/// [color] 图标颜色,如果不指定则使用默认颜色
|
||||
/// [useHighQuality] 是否使用高质量渲染,默认为true
|
||||
static Widget getProviderIcon(
|
||||
String provider, {
|
||||
double size = 24, // 提高默认尺寸
|
||||
Color? color,
|
||||
bool useHighQuality = true,
|
||||
}) {
|
||||
final normalizedProvider = provider.toLowerCase().trim();
|
||||
final iconPath = _providerIconPaths[normalizedProvider];
|
||||
|
||||
if (iconPath != null) {
|
||||
// 首先尝试加载 SVG 格式
|
||||
if (iconPath.endsWith('.svg')) {
|
||||
return SvgPicture.asset(
|
||||
iconPath,
|
||||
width: size,
|
||||
height: size,
|
||||
colorFilter: color != null
|
||||
? ColorFilter.mode(color, BlendMode.srcIn)
|
||||
: null,
|
||||
placeholderBuilder: (context) => _getDefaultIcon(
|
||||
provider,
|
||||
size: size,
|
||||
color: color,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 优化的 PNG 加载配置
|
||||
return Image.asset(
|
||||
iconPath,
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.contain,
|
||||
color: color,
|
||||
// 启用高质量过滤器,减少模糊
|
||||
filterQuality: useHighQuality ? FilterQuality.high : FilterQuality.medium,
|
||||
// 禁用抗锯齿可能导致的模糊
|
||||
isAntiAlias: true,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return _getDefaultIcon(provider, size: size, color: color);
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 如果没有找到对应图标,使用默认图标
|
||||
return _getDefaultIcon(provider, size: size, color: color);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取提供商图标(指定尺寸版本)
|
||||
/// 对于不同使用场景提供不同的尺寸建议
|
||||
static Widget getProviderIconForContext(
|
||||
String provider, {
|
||||
required IconSize iconSize,
|
||||
Color? color,
|
||||
}) {
|
||||
double size;
|
||||
switch (iconSize) {
|
||||
case IconSize.small:
|
||||
size = 16;
|
||||
break;
|
||||
case IconSize.medium:
|
||||
size = 24;
|
||||
break;
|
||||
case IconSize.large:
|
||||
size = 32;
|
||||
break;
|
||||
case IconSize.extraLarge:
|
||||
size = 48;
|
||||
break;
|
||||
}
|
||||
|
||||
return getProviderIcon(
|
||||
provider,
|
||||
size: size,
|
||||
color: color,
|
||||
useHighQuality: true,
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取提供商默认颜色
|
||||
static Color getProviderColor(String provider) {
|
||||
final normalizedProvider = provider.toLowerCase().trim();
|
||||
return _providerColors[normalizedProvider] ?? Colors.grey;
|
||||
}
|
||||
|
||||
/// 获取默认图标(当找不到对应图标时使用)
|
||||
static Widget _getDefaultIcon(
|
||||
String provider, {
|
||||
required double size,
|
||||
Color? color,
|
||||
}) {
|
||||
final normalizedProvider = provider.toLowerCase().trim();
|
||||
|
||||
IconData iconData;
|
||||
Color iconColor = color ?? getProviderColor(provider);
|
||||
|
||||
// 根据提供商名称选择合适的Material Icon作为备用
|
||||
if (normalizedProvider.contains('openai') ||
|
||||
normalizedProvider.contains('gpt') ||
|
||||
normalizedProvider.contains('chatgpt')) {
|
||||
iconData = Icons.auto_awesome;
|
||||
} else if (normalizedProvider.contains('anthropic') ||
|
||||
normalizedProvider.contains('claude')) {
|
||||
iconData = Icons.psychology;
|
||||
} else if (normalizedProvider.contains('google') ||
|
||||
normalizedProvider.contains('gemini') ||
|
||||
normalizedProvider.contains('bard')) {
|
||||
iconData = Icons.star;
|
||||
} else if (normalizedProvider.contains('openrouter')) {
|
||||
iconData = Icons.router;
|
||||
} else if (normalizedProvider.contains('ollama') ||
|
||||
normalizedProvider.contains('local')) {
|
||||
iconData = Icons.computer;
|
||||
} else if (normalizedProvider.contains('microsoft') ||
|
||||
normalizedProvider.contains('azure') ||
|
||||
normalizedProvider.contains('copilot')) {
|
||||
iconData = Icons.science;
|
||||
} else if (normalizedProvider.contains('meta') ||
|
||||
normalizedProvider.contains('llama') ||
|
||||
normalizedProvider.contains('facebook')) {
|
||||
iconData = Icons.groups;
|
||||
} else if (normalizedProvider.contains('bytedance') ||
|
||||
normalizedProvider.contains('doubao')) {
|
||||
iconData = Icons.smart_toy;
|
||||
} else if (normalizedProvider.contains('zhipu') ||
|
||||
normalizedProvider.contains('glm')) {
|
||||
iconData = Icons.lightbulb;
|
||||
} else if (normalizedProvider.contains('qwen') ||
|
||||
normalizedProvider.contains('tongyi') ||
|
||||
normalizedProvider.contains('alibaba')) {
|
||||
iconData = Icons.cloud;
|
||||
} else if (normalizedProvider.contains('deepseek')) {
|
||||
iconData = Icons.search;
|
||||
} else if (normalizedProvider.contains('mistral')) {
|
||||
iconData = Icons.air;
|
||||
} else if (normalizedProvider.contains('silicon')) {
|
||||
iconData = Icons.memory;
|
||||
} else if (normalizedProvider.contains('perplexity')) {
|
||||
iconData = Icons.quiz;
|
||||
} else if (normalizedProvider.contains('huggingface') ||
|
||||
normalizedProvider.contains('hf')) {
|
||||
iconData = Icons.emoji_emotions;
|
||||
} else if (normalizedProvider.contains('stability') ||
|
||||
normalizedProvider.contains('stable')) {
|
||||
iconData = Icons.image;
|
||||
} else if (normalizedProvider.contains('midjourney') ||
|
||||
normalizedProvider.contains('mj')) {
|
||||
iconData = Icons.palette;
|
||||
} else if (normalizedProvider.contains('xai') ||
|
||||
normalizedProvider.contains('grok')) {
|
||||
iconData = Icons.explore;
|
||||
} else if (normalizedProvider.contains('groq')) {
|
||||
iconData = Icons.speed;
|
||||
} else if (normalizedProvider.contains('together')) {
|
||||
iconData = Icons.group_work;
|
||||
} else if (normalizedProvider.contains('fireworks')) {
|
||||
iconData = Icons.celebration;
|
||||
} else if (normalizedProvider.contains('cohere')) {
|
||||
iconData = Icons.link;
|
||||
} else if (normalizedProvider.contains('replicate')) {
|
||||
iconData = Icons.replay;
|
||||
} else {
|
||||
iconData = Icons.api;
|
||||
}
|
||||
|
||||
return Icon(
|
||||
iconData,
|
||||
color: iconColor,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
|
||||
/// 检查是否支持某个提供商
|
||||
static bool isSupported(String provider) {
|
||||
final normalizedProvider = provider.toLowerCase().trim();
|
||||
return _providerIconPaths.containsKey(normalizedProvider);
|
||||
}
|
||||
|
||||
/// 获取所有支持的提供商列表
|
||||
static List<String> getSupportedProviders() {
|
||||
return _providerIconPaths.keys.toList();
|
||||
}
|
||||
|
||||
/// 获取提供商的显示名称
|
||||
static String getProviderDisplayName(String provider) {
|
||||
final normalizedProvider = provider.toLowerCase().trim();
|
||||
|
||||
const displayNames = {
|
||||
'openai': 'OpenAI',
|
||||
'chatgpt': 'ChatGPT',
|
||||
'gpt': 'GPT',
|
||||
'anthropic': 'Anthropic',
|
||||
'claude': 'Claude',
|
||||
'google': 'Google',
|
||||
'gemini': 'Gemini',
|
||||
'bard': 'Bard',
|
||||
'microsoft': 'Microsoft',
|
||||
'azure': 'Azure',
|
||||
'copilot': 'Copilot',
|
||||
'meta': 'Meta',
|
||||
'llama': 'Llama',
|
||||
'facebook': 'Facebook',
|
||||
'bytedance': '字节跳动',
|
||||
'doubao': '豆包',
|
||||
'zhipu': '智谱AI',
|
||||
'glm': 'GLM',
|
||||
'qwen': '通义千问',
|
||||
'tongyi': '通义千问',
|
||||
'alibaba': '阿里巴巴',
|
||||
'deepseek': 'DeepSeek',
|
||||
'mistral': 'Mistral',
|
||||
'siliconcloud': '硅基流动',
|
||||
'siliconflow': '硅基流动',
|
||||
'perplexity': 'Perplexity',
|
||||
'huggingface': 'Hugging Face',
|
||||
'hf': 'Hugging Face',
|
||||
'stability': 'Stability AI',
|
||||
'stable-diffusion': 'Stable Diffusion',
|
||||
'openrouter': 'OpenRouter',
|
||||
'ollama': 'Ollama',
|
||||
'xai': 'xAI',
|
||||
'grok': 'Grok',
|
||||
'x-ai': 'xAI',
|
||||
'X-ai': 'xAI',
|
||||
'midjourney': 'Midjourney',
|
||||
'mj': 'Midjourney',
|
||||
'groq': 'Groq',
|
||||
'together': 'Together AI',
|
||||
'together.ai': 'Together AI',
|
||||
'fireworks': 'Fireworks AI',
|
||||
'cohere': 'Cohere',
|
||||
'replicate': 'Replicate',
|
||||
'lm-studio': 'LM Studio',
|
||||
'lmstudio': 'LM Studio',
|
||||
'localai': 'LocalAI',
|
||||
'local': 'Local',
|
||||
};
|
||||
|
||||
return displayNames[normalizedProvider] ?? _capitalizeFirst(provider);
|
||||
}
|
||||
|
||||
/// 首字母大写
|
||||
static String _capitalizeFirst(String text) {
|
||||
if (text.isEmpty) return text;
|
||||
return text[0].toUpperCase() + text.substring(1);
|
||||
}
|
||||
}
|
||||
63
AINoval/lib/firebase_options.dart
Normal file
63
AINoval/lib/firebase_options.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
// File generated by FlutterFire CLI.
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
|
||||
import 'package:flutter/foundation.dart'
|
||||
show defaultTargetPlatform, kIsWeb, TargetPlatform;
|
||||
|
||||
/// Default [FirebaseOptions] for use with your Firebase apps.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// import 'firebase_options.dart';
|
||||
/// // ...
|
||||
/// await Firebase.initializeApp(
|
||||
/// options: DefaultFirebaseOptions.currentPlatform,
|
||||
/// );
|
||||
/// ```
|
||||
class DefaultFirebaseOptions {
|
||||
static FirebaseOptions get currentPlatform {
|
||||
if (kIsWeb) {
|
||||
return web;
|
||||
}
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for android - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
case TargetPlatform.iOS:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for ios - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
case TargetPlatform.macOS:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for macos - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
case TargetPlatform.windows:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for windows - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
case TargetPlatform.linux:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions have not been configured for linux - '
|
||||
'you can reconfigure this by running the FlutterFire CLI again.',
|
||||
);
|
||||
default:
|
||||
throw UnsupportedError(
|
||||
'DefaultFirebaseOptions are not supported for this platform.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static const FirebaseOptions web = FirebaseOptions(
|
||||
apiKey: 'AIzaSyCEJKLWArRyG87mt9XYCvMzkpq0FTHOCZ4',
|
||||
appId: '1:209076525028:web:d32d81e3fec013855319f1',
|
||||
messagingSenderId: '209076525028',
|
||||
projectId: 'ainovalwritergit',
|
||||
authDomain: 'ainovalwritergit.firebaseapp.com',
|
||||
storageBucket: 'ainovalwritergit.firebasestorage.app',
|
||||
);
|
||||
}
|
||||
34
AINoval/lib/l10n/app_en.arb
Normal file
34
AINoval/lib/l10n/app_en.arb
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"appTitle": "AI Novel Assistant",
|
||||
"homeTitle": "My Novels",
|
||||
"createNovel": "Create New Novel",
|
||||
"importNovel": "Import Novel",
|
||||
"editNovel": "Edit",
|
||||
"deleteNovel": "Delete",
|
||||
"deleteConfirmation": "Are you sure you want to delete '{title}'? This action cannot be undone.",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"novelTitle": "Novel Title",
|
||||
"novelTitleHint": "Enter novel title",
|
||||
"seriesName": "Series Name (Optional)",
|
||||
"seriesNameHint": "If part of a series, enter series name",
|
||||
"create": "Create",
|
||||
"lastEdited": "Last edited: {date}",
|
||||
"wordCount": "{count} words",
|
||||
"completionPercentage": "Completion: {percentage}%",
|
||||
"noNovels": "No novels yet. Click the button in the bottom right to create one.",
|
||||
"retry": "Retry",
|
||||
"loadingError": "Loading failed: {message}",
|
||||
"unknownState": "Unknown state",
|
||||
"save": "Save",
|
||||
"saved": "Saved",
|
||||
"editorSettings": "Editor Settings",
|
||||
"startWriting": "Start writing...",
|
||||
"wordCountTitle": "Word Count",
|
||||
"charactersWithSpaces": "Characters (with spaces)",
|
||||
"charactersNoSpaces": "Characters (no spaces)",
|
||||
"paragraphs": "Paragraphs",
|
||||
"readTime": "Estimated reading time",
|
||||
"minutes": "minutes",
|
||||
"close": "Close"
|
||||
}
|
||||
321
AINoval/lib/l10n/app_localizations.dart
Normal file
321
AINoval/lib/l10n/app_localizations.dart
Normal file
@@ -0,0 +1,321 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations_en.dart';
|
||||
import 'app_localizations_zh.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// Callers can lookup localized strings with an instance of AppLocalizations
|
||||
/// returned by `AppLocalizations.of(context)`.
|
||||
///
|
||||
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
||||
/// `localizationDelegates` list, and the locales they support in the app's
|
||||
/// `supportedLocales` list. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'l10n/app_localizations.dart';
|
||||
///
|
||||
/// return MaterialApp(
|
||||
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
/// supportedLocales: AppLocalizations.supportedLocales,
|
||||
/// home: MyApplicationHome(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ## Update pubspec.yaml
|
||||
///
|
||||
/// Please make sure to update your pubspec.yaml to include the following
|
||||
/// packages:
|
||||
///
|
||||
/// ```yaml
|
||||
/// dependencies:
|
||||
/// # Internationalization support.
|
||||
/// flutter_localizations:
|
||||
/// sdk: flutter
|
||||
/// intl: any # Use the pinned version from flutter_localizations
|
||||
///
|
||||
/// # Rest of dependencies
|
||||
/// ```
|
||||
///
|
||||
/// ## iOS Applications
|
||||
///
|
||||
/// iOS applications define key application metadata, including supported
|
||||
/// locales, in an Info.plist file that is built into the application bundle.
|
||||
/// To configure the locales supported by your app, you’ll need to edit this
|
||||
/// file.
|
||||
///
|
||||
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
||||
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
||||
/// project’s Runner folder.
|
||||
///
|
||||
/// Next, select the Information Property List item, select Add Item from the
|
||||
/// Editor menu, then select Localizations from the pop-up menu.
|
||||
///
|
||||
/// Select and expand the newly-created Localizations item then, for each
|
||||
/// locale your application supports, add a new item and select the locale
|
||||
/// you wish to add from the pop-up menu in the Value field. This list should
|
||||
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||
/// property.
|
||||
abstract class AppLocalizations {
|
||||
AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
static AppLocalizations? of(BuildContext context) {
|
||||
return Localizations.of<AppLocalizations>(context, AppLocalizations);
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
|
||||
|
||||
/// A list of this localizations delegate along with the default localizations
|
||||
/// delegates.
|
||||
///
|
||||
/// Returns a list of localizations delegates containing this delegate along with
|
||||
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
||||
/// and GlobalWidgetsLocalizations.delegate.
|
||||
///
|
||||
/// Additional delegates can be added by appending to this list in
|
||||
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
Locale('en'),
|
||||
Locale('zh')
|
||||
];
|
||||
|
||||
/// No description provided for @appTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'AI Novel Assistant'**
|
||||
String get appTitle;
|
||||
|
||||
/// No description provided for @homeTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'My Novels'**
|
||||
String get homeTitle;
|
||||
|
||||
/// No description provided for @createNovel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create New Novel'**
|
||||
String get createNovel;
|
||||
|
||||
/// No description provided for @importNovel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Import Novel'**
|
||||
String get importNovel;
|
||||
|
||||
/// No description provided for @editNovel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit'**
|
||||
String get editNovel;
|
||||
|
||||
/// No description provided for @deleteNovel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete'**
|
||||
String get deleteNovel;
|
||||
|
||||
/// No description provided for @deleteConfirmation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Are you sure you want to delete \'{title}\'? This action cannot be undone.'**
|
||||
String deleteConfirmation(Object title);
|
||||
|
||||
/// No description provided for @cancel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cancel'**
|
||||
String get cancel;
|
||||
|
||||
/// No description provided for @confirm.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Confirm'**
|
||||
String get confirm;
|
||||
|
||||
/// No description provided for @novelTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Novel Title'**
|
||||
String get novelTitle;
|
||||
|
||||
/// No description provided for @novelTitleHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter novel title'**
|
||||
String get novelTitleHint;
|
||||
|
||||
/// No description provided for @seriesName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Series Name (Optional)'**
|
||||
String get seriesName;
|
||||
|
||||
/// No description provided for @seriesNameHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'If part of a series, enter series name'**
|
||||
String get seriesNameHint;
|
||||
|
||||
/// No description provided for @create.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create'**
|
||||
String get create;
|
||||
|
||||
/// No description provided for @lastEdited.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last edited: {date}'**
|
||||
String lastEdited(Object date);
|
||||
|
||||
/// No description provided for @wordCount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count} words'**
|
||||
String wordCount(Object count);
|
||||
|
||||
/// No description provided for @completionPercentage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Completion: {percentage}%'**
|
||||
String completionPercentage(Object percentage);
|
||||
|
||||
/// No description provided for @noNovels.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No novels yet. Click the button in the bottom right to create one.'**
|
||||
String get noNovels;
|
||||
|
||||
/// No description provided for @retry.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Retry'**
|
||||
String get retry;
|
||||
|
||||
/// No description provided for @loadingError.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Loading failed: {message}'**
|
||||
String loadingError(Object message);
|
||||
|
||||
/// No description provided for @unknownState.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unknown state'**
|
||||
String get unknownState;
|
||||
|
||||
/// No description provided for @save.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save'**
|
||||
String get save;
|
||||
|
||||
/// No description provided for @saved.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Saved'**
|
||||
String get saved;
|
||||
|
||||
/// No description provided for @editorSettings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Editor Settings'**
|
||||
String get editorSettings;
|
||||
|
||||
/// No description provided for @startWriting.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Start writing...'**
|
||||
String get startWriting;
|
||||
|
||||
/// No description provided for @wordCountTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Word Count'**
|
||||
String get wordCountTitle;
|
||||
|
||||
/// No description provided for @charactersWithSpaces.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Characters (with spaces)'**
|
||||
String get charactersWithSpaces;
|
||||
|
||||
/// No description provided for @charactersNoSpaces.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Characters (no spaces)'**
|
||||
String get charactersNoSpaces;
|
||||
|
||||
/// No description provided for @paragraphs.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Paragraphs'**
|
||||
String get paragraphs;
|
||||
|
||||
/// No description provided for @readTime.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Estimated reading time'**
|
||||
String get readTime;
|
||||
|
||||
/// No description provided for @minutes.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'minutes'**
|
||||
String get minutes;
|
||||
|
||||
/// No description provided for @close.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Close'**
|
||||
String get close;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
const _AppLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
Future<AppLocalizations> load(Locale locale) {
|
||||
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) => <String>['en', 'zh'].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||
}
|
||||
|
||||
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||
|
||||
|
||||
// Lookup logic when only language code is specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'en': return AppLocalizationsEn();
|
||||
case 'zh': return AppLocalizationsZh();
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.'
|
||||
);
|
||||
}
|
||||
116
AINoval/lib/l10n/app_localizations_en.dart
Normal file
116
AINoval/lib/l10n/app_localizations_en.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for English (`en`).
|
||||
class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'AI Novel Assistant';
|
||||
|
||||
@override
|
||||
String get homeTitle => 'My Novels';
|
||||
|
||||
@override
|
||||
String get createNovel => 'Create New Novel';
|
||||
|
||||
@override
|
||||
String get importNovel => 'Import Novel';
|
||||
|
||||
@override
|
||||
String get editNovel => 'Edit';
|
||||
|
||||
@override
|
||||
String get deleteNovel => 'Delete';
|
||||
|
||||
@override
|
||||
String deleteConfirmation(Object title) {
|
||||
return 'Are you sure you want to delete \'$title\'? This action cannot be undone.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancel => 'Cancel';
|
||||
|
||||
@override
|
||||
String get confirm => 'Confirm';
|
||||
|
||||
@override
|
||||
String get novelTitle => 'Novel Title';
|
||||
|
||||
@override
|
||||
String get novelTitleHint => 'Enter novel title';
|
||||
|
||||
@override
|
||||
String get seriesName => 'Series Name (Optional)';
|
||||
|
||||
@override
|
||||
String get seriesNameHint => 'If part of a series, enter series name';
|
||||
|
||||
@override
|
||||
String get create => 'Create';
|
||||
|
||||
@override
|
||||
String lastEdited(Object date) {
|
||||
return 'Last edited: $date';
|
||||
}
|
||||
|
||||
@override
|
||||
String wordCount(Object count) {
|
||||
return '$count words';
|
||||
}
|
||||
|
||||
@override
|
||||
String completionPercentage(Object percentage) {
|
||||
return 'Completion: $percentage%';
|
||||
}
|
||||
|
||||
@override
|
||||
String get noNovels => 'No novels yet. Click the button in the bottom right to create one.';
|
||||
|
||||
@override
|
||||
String get retry => 'Retry';
|
||||
|
||||
@override
|
||||
String loadingError(Object message) {
|
||||
return 'Loading failed: $message';
|
||||
}
|
||||
|
||||
@override
|
||||
String get unknownState => 'Unknown state';
|
||||
|
||||
@override
|
||||
String get save => 'Save';
|
||||
|
||||
@override
|
||||
String get saved => 'Saved';
|
||||
|
||||
@override
|
||||
String get editorSettings => 'Editor Settings';
|
||||
|
||||
@override
|
||||
String get startWriting => 'Start writing...';
|
||||
|
||||
@override
|
||||
String get wordCountTitle => 'Word Count';
|
||||
|
||||
@override
|
||||
String get charactersWithSpaces => 'Characters (with spaces)';
|
||||
|
||||
@override
|
||||
String get charactersNoSpaces => 'Characters (no spaces)';
|
||||
|
||||
@override
|
||||
String get paragraphs => 'Paragraphs';
|
||||
|
||||
@override
|
||||
String get readTime => 'Estimated reading time';
|
||||
|
||||
@override
|
||||
String get minutes => 'minutes';
|
||||
|
||||
@override
|
||||
String get close => 'Close';
|
||||
}
|
||||
116
AINoval/lib/l10n/app_localizations_zh.dart
Normal file
116
AINoval/lib/l10n/app_localizations_zh.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for Chinese (`zh`).
|
||||
class AppLocalizationsZh extends AppLocalizations {
|
||||
AppLocalizationsZh([String locale = 'zh']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'AI小说助手';
|
||||
|
||||
@override
|
||||
String get homeTitle => '我的小说';
|
||||
|
||||
@override
|
||||
String get createNovel => '创建新小说';
|
||||
|
||||
@override
|
||||
String get importNovel => '导入小说';
|
||||
|
||||
@override
|
||||
String get editNovel => '编辑';
|
||||
|
||||
@override
|
||||
String get deleteNovel => '删除';
|
||||
|
||||
@override
|
||||
String deleteConfirmation(Object title) {
|
||||
return '确定要删除《$title》吗?此操作不可撤销。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cancel => '取消';
|
||||
|
||||
@override
|
||||
String get confirm => '确定';
|
||||
|
||||
@override
|
||||
String get novelTitle => '小说标题';
|
||||
|
||||
@override
|
||||
String get novelTitleHint => '请输入小说标题';
|
||||
|
||||
@override
|
||||
String get seriesName => '系列名称 (可选)';
|
||||
|
||||
@override
|
||||
String get seriesNameHint => '如果是系列作品,请输入系列名称';
|
||||
|
||||
@override
|
||||
String get create => '创建';
|
||||
|
||||
@override
|
||||
String lastEdited(Object date) {
|
||||
return '上次编辑: $date';
|
||||
}
|
||||
|
||||
@override
|
||||
String wordCount(Object count) {
|
||||
return '$count字';
|
||||
}
|
||||
|
||||
@override
|
||||
String completionPercentage(Object percentage) {
|
||||
return '完成度: $percentage%';
|
||||
}
|
||||
|
||||
@override
|
||||
String get noNovels => '暂无小说,点击右下角按钮创建新小说';
|
||||
|
||||
@override
|
||||
String get retry => '重试';
|
||||
|
||||
@override
|
||||
String loadingError(Object message) {
|
||||
return '加载失败: $message';
|
||||
}
|
||||
|
||||
@override
|
||||
String get unknownState => '未知状态';
|
||||
|
||||
@override
|
||||
String get save => '保存';
|
||||
|
||||
@override
|
||||
String get saved => '已保存';
|
||||
|
||||
@override
|
||||
String get editorSettings => '编辑器设置';
|
||||
|
||||
@override
|
||||
String get startWriting => '开始您的创作...';
|
||||
|
||||
@override
|
||||
String get wordCountTitle => '字数统计';
|
||||
|
||||
@override
|
||||
String get charactersWithSpaces => '字符数(含空格)';
|
||||
|
||||
@override
|
||||
String get charactersNoSpaces => '字符数(不含空格)';
|
||||
|
||||
@override
|
||||
String get paragraphs => '段落数';
|
||||
|
||||
@override
|
||||
String get readTime => '预计阅读时间';
|
||||
|
||||
@override
|
||||
String get minutes => '分钟';
|
||||
|
||||
@override
|
||||
String get close => '关闭';
|
||||
}
|
||||
40
AINoval/lib/l10n/app_zh.arb
Normal file
40
AINoval/lib/l10n/app_zh.arb
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"appTitle": "AI小说助手",
|
||||
"homeTitle": "我的小说",
|
||||
"createNovel": "创建新小说",
|
||||
"importNovel": "导入小说",
|
||||
"editNovel": "编辑",
|
||||
"deleteNovel": "删除",
|
||||
"deleteConfirmation": "确定要删除《{title}》吗?此操作不可撤销。",
|
||||
"cancel": "取消",
|
||||
"confirm": "确定",
|
||||
"novelTitle": "小说标题",
|
||||
"novelTitleHint": "请输入小说标题",
|
||||
"seriesName": "系列名称 (可选)",
|
||||
"seriesNameHint": "如果是系列作品,请输入系列名称",
|
||||
"create": "创建",
|
||||
"lastEdited": "上次编辑: {date}",
|
||||
"wordCount": "{count}字",
|
||||
"completionPercentage": "完成度: {percentage}%",
|
||||
"noNovels": "暂无小说,点击右下角按钮创建新小说",
|
||||
"retry": "重试",
|
||||
"loadingError": "加载失败: {message}",
|
||||
"unknownState": "未知状态",
|
||||
"save": "保存",
|
||||
"saved": "已保存",
|
||||
"editorSettings": "编辑器设置",
|
||||
"startWriting": "开始您的创作...",
|
||||
"wordCountTitle": "字数统计",
|
||||
"charactersWithSpaces": "字符数(含空格)",
|
||||
"charactersNoSpaces": "字符数(不含空格)",
|
||||
"paragraphs": "段落数",
|
||||
"readTime": "预计阅读时间",
|
||||
"minutes": "分钟",
|
||||
"close": "关闭",
|
||||
"chatWithAI": "与AI聊天",
|
||||
"sendMessage": "发送消息",
|
||||
"typeMessage": "输入消息...",
|
||||
"newChat": "新对话",
|
||||
"loadingChat": "加载对话中...",
|
||||
"aiAssistant": "AI助手"
|
||||
}
|
||||
13
AINoval/lib/l10n/l10n.dart
Normal file
13
AINoval/lib/l10n/l10n.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ainoval/l10n/app_localizations.dart';
|
||||
|
||||
extension AppLocalizationsX on BuildContext {
|
||||
AppLocalizations get l10n => AppLocalizations.of(this)!;
|
||||
}
|
||||
|
||||
class L10n {
|
||||
static const all = [
|
||||
Locale('zh', 'CN'),
|
||||
Locale('en', 'US'),
|
||||
];
|
||||
}
|
||||
569
AINoval/lib/main.dart
Normal file
569
AINoval/lib/main.dart
Normal file
@@ -0,0 +1,569 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
|
||||
// <<< 导入 AiConfigBloc >>>
|
||||
import 'package:ainoval/blocs/ai_config/ai_config_bloc.dart';
|
||||
// 导入聊天相关的类
|
||||
import 'package:ainoval/blocs/auth/auth_bloc.dart';
|
||||
import 'package:ainoval/blocs/chat/chat_bloc.dart';
|
||||
import 'package:ainoval/blocs/credit/credit_bloc.dart';
|
||||
import 'package:ainoval/blocs/editor_version_bloc.dart';
|
||||
import 'package:ainoval/blocs/novel_list/novel_list_bloc.dart';
|
||||
import 'package:ainoval/blocs/public_models/public_models_bloc.dart';
|
||||
import 'package:ainoval/blocs/setting_generation/setting_generation_bloc.dart';
|
||||
import 'package:ainoval/config/app_config.dart'; // 引入 AppConfig
|
||||
import 'package:ainoval/l10n/l10n.dart';
|
||||
import 'package:ainoval/models/app_registration_config.dart';
|
||||
|
||||
// import 'package:ainoval/screens/novel_list/novel_list_screen.dart'; // 已删除,使用新页面
|
||||
import 'package:ainoval/screens/novel_list/novel_list_real_data_screen.dart' deferred as novel_list;
|
||||
import 'package:ainoval/services/api_service/base/api_client.dart';
|
||||
import 'package:ainoval/services/api_service/base/sse_client.dart';
|
||||
// <<< 移除未使用的 Codex Impl 引用 >>>
|
||||
// import 'package:ainoval/services/api_service/repositories/impl/codex_repository_impl.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/chat_repository.dart'; // <<< 导入接口
|
||||
// ApiService import might not be needed directly in main unless provided
|
||||
// import 'package:ainoval/services/api_service.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/impl/chat_repository_impl.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/impl/credit_repository_impl.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/impl/novel_repository_impl.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/impl/novel_setting_repository_impl.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/impl/public_model_repository_impl.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/impl/storage_repository_impl.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/impl/user_ai_model_config_repository_impl.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/impl/setting_generation_repository_impl.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/impl/universal_ai_repository_impl.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/impl/preset_aggregation_repository_impl.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/impl/ai_preset_repository_impl.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/impl/novel_snippet_repository_impl.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/novel_repository.dart'; // <<< 导入接口
|
||||
import 'package:ainoval/services/image_cache_service.dart';
|
||||
// import 'package:ainoval/services/api_service/repositories/novel_setting_repository.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/credit_repository.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/public_model_repository.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/storage_repository.dart';
|
||||
// <<< 导入 AI Config 仓库 >>>
|
||||
import 'package:ainoval/services/api_service/repositories/user_ai_model_config_repository.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/setting_generation_repository.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/universal_ai_repository.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/services/api_service/repositories/novel_snippet_repository.dart';
|
||||
import 'package:ainoval/services/auth_service.dart' as auth_service;
|
||||
import 'package:ainoval/services/local_storage_service.dart';
|
||||
import 'package:ainoval/services/novel_file_service.dart'; // 导入小说文件服务
|
||||
// import 'package:ainoval/services/websocket_service.dart';
|
||||
import 'package:ainoval/utils/web_theme.dart';
|
||||
import 'package:ainoval/utils/logger.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:ainoval/l10n/app_localizations.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/prompt_repository.dart';
|
||||
import 'package:ainoval/services/api_service/repositories/impl/prompt_repository_impl.dart';
|
||||
// 重复导入清理(下方已存在这些导入)
|
||||
import 'package:ainoval/blocs/universal_ai/universal_ai_bloc.dart';
|
||||
import 'package:ainoval/utils/navigation_logger.dart';
|
||||
import 'package:ainoval/blocs/prompt_new/prompt_new_bloc.dart';
|
||||
import 'package:ainoval/blocs/prompt_new/prompt_new_event.dart';
|
||||
import 'package:ainoval/blocs/theme/theme_bloc.dart';
|
||||
import 'package:ainoval/blocs/theme/theme_event.dart';
|
||||
import 'package:ainoval/blocs/theme/theme_state.dart';
|
||||
// 导入预设管理BLoC
|
||||
import 'package:ainoval/blocs/preset/preset_bloc.dart';
|
||||
import 'package:ainoval/blocs/preset/preset_event.dart';
|
||||
// 导入预设聚合仓储
|
||||
import 'package:ainoval/screens/unified_management/unified_management_screen.dart' deferred as unified_mgmt;
|
||||
|
||||
void main() {
|
||||
runZonedGuarded(() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Web 平台下:覆盖 Flutter 全局错误处理,避免 Inspector 在处理 JS 对象时报错
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
if (kIsWeb) {
|
||||
// 直接输出字符串化的异常信息,避免 DiagnosticsNode 转换
|
||||
debugPrint('FlutterError: ${details.exceptionAsString()}');
|
||||
if (details.stack != null) {
|
||||
debugPrint(details.stack.toString());
|
||||
}
|
||||
} else {
|
||||
FlutterError.presentError(details);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化日志系统
|
||||
AppLogger.init();
|
||||
|
||||
// 初始化Hive本地存储
|
||||
await Hive.initFlutter();
|
||||
|
||||
// 初始化注册配置
|
||||
await _initializeRegistrationConfig();
|
||||
|
||||
// 创建必要的资源文件夹 - 仅在非Web平台执行
|
||||
if (!kIsWeb) {
|
||||
await _createResourceDirectories();
|
||||
}
|
||||
|
||||
// 初始化LocalStorageService
|
||||
final localStorageService = LocalStorageService();
|
||||
await localStorageService.init();
|
||||
|
||||
// 创建AuthService
|
||||
final authServiceInstance = auth_service.AuthService();
|
||||
await authServiceInstance.init();
|
||||
|
||||
// 创建 ApiClient 实例并传入 AuthService
|
||||
final apiClient = ApiClient(authService: authServiceInstance);
|
||||
|
||||
// 创建 SseClient 实例 (单例模式)
|
||||
final sseClient = SseClient();
|
||||
/*
|
||||
// 创建ApiService (如果 ApiService 需要 ApiClient, 则传入)
|
||||
// 假设 ApiService 构造函数接受 apiClient (如果不需要则忽略)
|
||||
final apiService = ApiService(/* apiClient: apiClient */);
|
||||
|
||||
// 创建WebSocketService
|
||||
final webSocketService = WebSocketService(); */
|
||||
|
||||
// 创建NovelRepository (它不再需要MockDataService)
|
||||
final novelRepository = NovelRepositoryImpl(/* apiClient: apiClient */);
|
||||
|
||||
// 创建ChatRepository,并传入 ApiClient
|
||||
final chatRepository = ChatRepositoryImpl(
|
||||
apiClient: apiClient, // 使用直接创建的 apiClient
|
||||
);
|
||||
|
||||
// 创建StorageRepository实例
|
||||
final storageRepository = StorageRepositoryImpl(apiClient);
|
||||
|
||||
// 创建UserAIModelConfigRepository
|
||||
final userAIModelConfigRepository =
|
||||
UserAIModelConfigRepositoryImpl(apiClient: apiClient);
|
||||
|
||||
// 创建PublicModelRepository
|
||||
final publicModelRepository = PublicModelRepositoryImpl(apiClient: apiClient);
|
||||
|
||||
// 创建CreditRepository
|
||||
final creditRepository = CreditRepositoryImpl(apiClient: apiClient);
|
||||
|
||||
// 创建NovelSettingRepository
|
||||
final novelSettingRepository = NovelSettingRepositoryImpl(apiClient: apiClient);
|
||||
|
||||
|
||||
|
||||
// 创建PromptRepository
|
||||
final promptRepository = PromptRepositoryImpl(apiClient);
|
||||
|
||||
// 创建NovelFileService
|
||||
final novelFileService = NovelFileService(
|
||||
novelRepository: novelRepository,
|
||||
// editorRepository 暂时为空,可以后续添加
|
||||
);
|
||||
|
||||
// 创建NovelSnippetRepository
|
||||
final novelSnippetRepository = NovelSnippetRepositoryImpl(apiClient);
|
||||
|
||||
// 创建UniversalAIRepository
|
||||
final universalAIRepository = UniversalAIRepositoryImpl(apiClient: apiClient);
|
||||
|
||||
// 创建PresetAggregationRepository
|
||||
final presetAggregationRepository = PresetAggregationRepositoryImpl(apiClient);
|
||||
|
||||
// 创建AIPresetRepository
|
||||
final aiPresetRepository = AIPresetRepositoryImpl(apiClient: apiClient);
|
||||
|
||||
// 创建SettingGenerationRepository
|
||||
final settingGenerationRepository = SettingGenerationRepositoryImpl(
|
||||
apiClient: apiClient,
|
||||
sseClient: sseClient,
|
||||
);
|
||||
|
||||
// 初始化图片缓存服务(如需预热可在此调用)
|
||||
// ImageCacheService().prewarm();
|
||||
|
||||
AppLogger.i('Main', '应用程序初始化完成,准备启动界面');
|
||||
|
||||
runApp(MultiRepositoryProvider(
|
||||
providers: [
|
||||
RepositoryProvider<auth_service.AuthService>.value(
|
||||
value: authServiceInstance),
|
||||
RepositoryProvider<ApiClient>.value(value: apiClient),
|
||||
RepositoryProvider<NovelRepository>.value(value: novelRepository),
|
||||
RepositoryProvider<ChatRepository>.value(value: chatRepository),
|
||||
RepositoryProvider<StorageRepository>.value(value: storageRepository),
|
||||
RepositoryProvider<UserAIModelConfigRepository>.value(
|
||||
value: userAIModelConfigRepository),
|
||||
RepositoryProvider<PublicModelRepository>.value(
|
||||
value: publicModelRepository),
|
||||
RepositoryProvider<CreditRepository>.value(
|
||||
value: creditRepository),
|
||||
RepositoryProvider<LocalStorageService>.value(
|
||||
value: localStorageService),
|
||||
RepositoryProvider<PromptRepository>(
|
||||
create: (context) => promptRepository,
|
||||
),
|
||||
RepositoryProvider<NovelFileService>.value(
|
||||
value: novelFileService,
|
||||
),
|
||||
RepositoryProvider<NovelSnippetRepository>.value(
|
||||
value: novelSnippetRepository,
|
||||
),
|
||||
RepositoryProvider<UniversalAIRepository>.value(
|
||||
value: universalAIRepository,
|
||||
),
|
||||
RepositoryProvider<PresetAggregationRepository>.value(
|
||||
value: presetAggregationRepository,
|
||||
),
|
||||
RepositoryProvider<AIPresetRepository>.value(
|
||||
value: aiPresetRepository,
|
||||
),
|
||||
RepositoryProvider<SettingGenerationRepository>.value(
|
||||
value: settingGenerationRepository,
|
||||
),
|
||||
],
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<AuthBloc>(
|
||||
create: (context) => AuthBloc(
|
||||
authService: context.read<auth_service.AuthService>(),
|
||||
)..add(AuthInitialize()),
|
||||
),
|
||||
BlocProvider<NovelListBloc>(
|
||||
create: (context) => NovelListBloc(
|
||||
repository: context.read<NovelRepository>(),
|
||||
),
|
||||
),
|
||||
BlocProvider<AiConfigBloc>(
|
||||
create: (context) => AiConfigBloc(
|
||||
repository: context.read<UserAIModelConfigRepository>(),
|
||||
),
|
||||
),
|
||||
BlocProvider<PublicModelsBloc>(
|
||||
create: (context) => PublicModelsBloc(
|
||||
repository: context.read<PublicModelRepository>(),
|
||||
),
|
||||
),
|
||||
BlocProvider<CreditBloc>(
|
||||
create: (context) => CreditBloc(
|
||||
repository: context.read<CreditRepository>(),
|
||||
),
|
||||
),
|
||||
BlocProvider<SettingGenerationBloc>(
|
||||
create: (context) => SettingGenerationBloc(
|
||||
repository: context.read<SettingGenerationRepository>(),
|
||||
),
|
||||
),
|
||||
/*
|
||||
BlocProvider<ReaderBloc>(
|
||||
create: (context) => ReaderBloc(
|
||||
repository: context.read<NovelRepository>(),
|
||||
),
|
||||
),
|
||||
*/
|
||||
BlocProvider<ChatBloc>(
|
||||
create: (context) => ChatBloc(
|
||||
repository: context.read<ChatRepository>(),
|
||||
authService: context.read<auth_service.AuthService>(),
|
||||
aiConfigBloc: context.read<AiConfigBloc>(),
|
||||
publicModelsBloc: context.read<PublicModelsBloc>(),
|
||||
settingRepository: novelSettingRepository,
|
||||
snippetRepository: novelSnippetRepository,
|
||||
),
|
||||
),
|
||||
BlocProvider<EditorVersionBloc>(
|
||||
create: (context) => EditorVersionBloc(
|
||||
novelRepository: context.read<NovelRepository>(),
|
||||
),
|
||||
),
|
||||
BlocProvider<UniversalAIBloc>(
|
||||
create: (context) => UniversalAIBloc(
|
||||
repository: context.read<UniversalAIRepository>(),
|
||||
),
|
||||
),
|
||||
BlocProvider<PromptNewBloc>(
|
||||
create: (context) => PromptNewBloc(
|
||||
promptRepository: context.read<PromptRepository>(),
|
||||
),
|
||||
),
|
||||
BlocProvider<ThemeBloc>(
|
||||
create: (context) => ThemeBloc()..add(ThemeInitialize()),
|
||||
),
|
||||
BlocProvider<PresetBloc>(
|
||||
create: (context) => PresetBloc(
|
||||
aggregationRepository: context.read<PresetAggregationRepository>(),
|
||||
presetRepository: context.read<AIPresetRepository>(),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: const MyApp(),
|
||||
),
|
||||
));
|
||||
}, (error, stack) {
|
||||
// 兜底:捕获所有未处理异常并记录,避免在 Web 上出现 LegacyJavaScriptObject -> DiagnosticsNode 的崩溃
|
||||
AppLogger.e('Uncaught', '未捕获异常: $error', error, stack);
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化注册配置
|
||||
Future<void> _initializeRegistrationConfig() async {
|
||||
try {
|
||||
// 确保注册配置已初始化,设置默认值
|
||||
// 默认开启邮箱注册和手机注册,需要验证码验证
|
||||
final phoneEnabled = await AppRegistrationConfig.isPhoneRegistrationEnabled();
|
||||
final emailEnabled = await AppRegistrationConfig.isEmailRegistrationEnabled();
|
||||
final verificationRequired = await AppRegistrationConfig.isVerificationRequired();
|
||||
|
||||
AppLogger.i('Registration',
|
||||
'📝 注册配置已加载 - 邮箱注册: $emailEnabled, 手机注册: $phoneEnabled, 验证码验证: $verificationRequired');
|
||||
|
||||
// 如果没有任何注册方式可用,启用默认的邮箱注册
|
||||
if (!phoneEnabled && !emailEnabled) {
|
||||
await AppRegistrationConfig.setEmailRegistrationEnabled(true);
|
||||
AppLogger.i('Registration', '🔧 已自动启用邮箱注册功能');
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.e('Registration', '初始化注册配置失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建资源文件夹
|
||||
Future<void> _createResourceDirectories() async {
|
||||
try {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final assetsDir = Directory('${appDir.path}/assets');
|
||||
final imagesDir = Directory('${assetsDir.path}/images');
|
||||
final iconsDir = Directory('${assetsDir.path}/icons');
|
||||
|
||||
// 创建资源目录
|
||||
if (!await assetsDir.exists()) {
|
||||
await assetsDir.create(recursive: true);
|
||||
}
|
||||
|
||||
// 创建图像目录
|
||||
if (!await imagesDir.exists()) {
|
||||
await imagesDir.create(recursive: true);
|
||||
}
|
||||
|
||||
// 创建图标目录
|
||||
if (!await iconsDir.exists()) {
|
||||
await iconsDir.create(recursive: true);
|
||||
}
|
||||
|
||||
AppLogger.i('ResourceDir', '资源文件夹创建成功');
|
||||
} catch (e) {
|
||||
AppLogger.e('ResourceDir', '创建资源文件夹失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
bool _postLoginBootstrapped = false;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
ImageCacheService().clearCache();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
switch (state) {
|
||||
case AppLifecycleState.paused:
|
||||
case AppLifecycleState.detached:
|
||||
// 应用进入后台或被关闭时清理图片缓存
|
||||
ImageCacheService().clearCache();
|
||||
break;
|
||||
case AppLifecycleState.resumed:
|
||||
// 应用恢复时可以预加载一些图片
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ThemeBloc, ThemeState>(
|
||||
builder: (context, themeState) {
|
||||
return ValueListenableBuilder<String>(
|
||||
valueListenable: WebTheme.variantListenable,
|
||||
builder: (context, variant, _) {
|
||||
// 根据当前变体重建主题
|
||||
return MaterialApp(
|
||||
navigatorObservers: [NavigationLogger()],
|
||||
title: 'AINoval',
|
||||
theme: WebTheme.buildLightTheme(),
|
||||
darkTheme: WebTheme.buildDarkTheme(),
|
||||
themeMode: themeState.themeMode,
|
||||
initialRoute: '/',
|
||||
routes: {
|
||||
'/': (context) => BlocConsumer<AuthBloc, AuthState>(
|
||||
listenWhen: (prev, curr) =>
|
||||
curr is AuthAuthenticated || curr is AuthUnauthenticated,
|
||||
listener: (context, state) {
|
||||
AppLogger.i('MyApp', '🔔 AuthBloc状态变化: ${state.runtimeType}');
|
||||
|
||||
if (state is AuthAuthenticated) {
|
||||
if (_postLoginBootstrapped) {
|
||||
AppLogger.i('MyApp', '🔁 已完成登录后的初始化,跳过重复触发');
|
||||
}
|
||||
final userId = AppConfig.userId;
|
||||
if (userId != null) {
|
||||
AppLogger.i('MyApp',
|
||||
'User authenticated, loading AiConfigs, PublicModels, Credits, Novels, Presets and PromptPackages for user $userId');
|
||||
// 并行加载用户AI配置、公共模型和用户积分
|
||||
if (!_postLoginBootstrapped) {
|
||||
context.read<AiConfigBloc>().add(LoadAiConfigs(userId: userId));
|
||||
context.read<PublicModelsBloc>().add(const LoadPublicModels());
|
||||
// 每次登录都强制重新加载积分,避免复用上个账号缓存
|
||||
context.read<CreditBloc>().add(const LoadUserCredits());
|
||||
// 用户登录成功后,加载一次小说列表数据(仅在未加载时)
|
||||
final novelState = context.read<NovelListBloc>().state;
|
||||
if (novelState is! NovelListLoaded) {
|
||||
context.read<NovelListBloc>().add(LoadNovels());
|
||||
}
|
||||
// 预设与提示词包
|
||||
context.read<PresetBloc>().add(const LoadAllPresetData());
|
||||
context.read<PromptNewBloc>().add(const LoadAllPromptPackages());
|
||||
_postLoginBootstrapped = true;
|
||||
}
|
||||
} else {
|
||||
AppLogger.e('MyApp',
|
||||
'User authenticated but userId is null in AppConfig!');
|
||||
}
|
||||
} else if (state is AuthUnauthenticated) {
|
||||
AppLogger.i('MyApp', '✅ 用户已退出登录,清理所有BLoC状态');
|
||||
_postLoginBootstrapped = false;
|
||||
|
||||
// 清理所有BLoC状态,停止进行中的请求
|
||||
try {
|
||||
// 重置 AI 配置,避免跨用户复用本地缓存/内存状态
|
||||
context.read<AiConfigBloc>().add(const ResetAiConfigs());
|
||||
} catch (e) {
|
||||
AppLogger.w('MyApp', '重置AiConfigBloc状态失败', e);
|
||||
}
|
||||
try {
|
||||
// 清理小说列表状态
|
||||
context.read<NovelListBloc>().add(ClearNovels());
|
||||
AppLogger.i('MyApp', '✅ NovelListBloc状态已清理');
|
||||
} catch (e) {
|
||||
AppLogger.w('MyApp', '清理NovelListBloc状态失败', e);
|
||||
}
|
||||
|
||||
// 清空积分显示为游客(0)
|
||||
try {
|
||||
context.read<CreditBloc>().add(const ClearCredits());
|
||||
AppLogger.i('MyApp', '✅ CreditBloc状态已清空');
|
||||
} catch (e) {
|
||||
AppLogger.w('MyApp', '清空CreditBloc状态失败', e);
|
||||
}
|
||||
|
||||
// 清除用户显示名称为游客
|
||||
AppConfig.setUsername(null);
|
||||
AppConfig.setUserId(null);
|
||||
AppConfig.setAuthToken(null);
|
||||
// 可以根据需要添加其他BLoC的清理逻辑
|
||||
// 但暂时先清理最关键的小说列表,避免404请求
|
||||
} else if (state is AuthLoading) {
|
||||
AppLogger.i('MyApp', '⏳ 认证状态加载中...');
|
||||
} else if (state is AuthError) {
|
||||
AppLogger.w('MyApp', '❌ 认证错误: ${state.message}');
|
||||
}
|
||||
},
|
||||
buildWhen: (prev, curr) =>
|
||||
curr is AuthAuthenticated || curr is AuthUnauthenticated,
|
||||
builder: (context, state) {
|
||||
AppLogger.i('MyApp', '🏗️ 构建UI,当前状态: ${state.runtimeType}');
|
||||
|
||||
if (state is AuthAuthenticated) {
|
||||
AppLogger.i(
|
||||
'MyApp', '📚 显示小说列表界面');
|
||||
// 🚀 登录成功后异步加载并应用用户的主题变体,确保全局组件使用保存的主题色
|
||||
final userId = AppConfig.userId;
|
||||
if (userId != null) {
|
||||
() async {
|
||||
try {
|
||||
final settings = await NovelRepositoryImpl.getInstance().getUserEditorSettings(userId);
|
||||
WebTheme.applyVariant(settings.themeVariant);
|
||||
AppLogger.i('MyApp', '🎨 已应用用户主题变体: ${settings.themeVariant}');
|
||||
} catch (e) {
|
||||
AppLogger.w('MyApp', '无法应用用户主题变体: $e');
|
||||
}
|
||||
}();
|
||||
}
|
||||
// 异步加载小说列表页面,实现代码分割
|
||||
return FutureBuilder(
|
||||
future: novel_list.loadLibrary(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return novel_list.NovelListRealDataScreen();
|
||||
}
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
// 未登录:默认展示小说列表的“游客模式”界面,受控于页面内的鉴权弹窗
|
||||
return FutureBuilder(
|
||||
future: novel_list.loadLibrary(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return novel_list.NovelListRealDataScreen();
|
||||
}
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
'/unified-management': (context) => FutureBuilder(
|
||||
future: unified_mgmt.loadLibrary(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return unified_mgmt.UnifiedManagementScreen();
|
||||
}
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
),
|
||||
|
||||
|
||||
},
|
||||
debugShowCheckedModeBanner: false,
|
||||
|
||||
// 添加完整的本地化支持
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: L10n.all,
|
||||
locale: const Locale('zh', 'CN'), // 设置默认语言为中文
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
85
AINoval/lib/models/admin/admin_auth_models.dart
Normal file
85
AINoval/lib/models/admin/admin_auth_models.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// 管理员认证请求
|
||||
class AdminAuthRequest extends Equatable {
|
||||
final String username;
|
||||
final String password;
|
||||
|
||||
const AdminAuthRequest({
|
||||
required this.username,
|
||||
required this.password,
|
||||
});
|
||||
|
||||
factory AdminAuthRequest.fromJson(Map<String, dynamic> json) {
|
||||
return AdminAuthRequest(
|
||||
username: json['username'] as String,
|
||||
password: json['password'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'username': username,
|
||||
'password': password,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [username, password];
|
||||
}
|
||||
|
||||
/// 管理员认证响应
|
||||
class AdminAuthResponse extends Equatable {
|
||||
final String token;
|
||||
final String refreshToken;
|
||||
final String userId;
|
||||
final String username;
|
||||
final String? displayName;
|
||||
final List<String> roles;
|
||||
final List<String> permissions;
|
||||
|
||||
const AdminAuthResponse({
|
||||
required this.token,
|
||||
required this.refreshToken,
|
||||
required this.userId,
|
||||
required this.username,
|
||||
this.displayName,
|
||||
required this.roles,
|
||||
required this.permissions,
|
||||
});
|
||||
|
||||
factory AdminAuthResponse.fromJson(Map<String, dynamic> json) {
|
||||
return AdminAuthResponse(
|
||||
token: json['token'] as String,
|
||||
refreshToken: json['refreshToken'] as String,
|
||||
userId: json['userId'] as String,
|
||||
username: json['username'] as String,
|
||||
displayName: json['displayName'] as String?,
|
||||
roles: List<String>.from(json['roles'] as List? ?? []),
|
||||
permissions: List<String>.from(json['permissions'] as List? ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'token': token,
|
||||
'refreshToken': refreshToken,
|
||||
'userId': userId,
|
||||
'username': username,
|
||||
'displayName': displayName,
|
||||
'roles': roles,
|
||||
'permissions': permissions,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
token,
|
||||
refreshToken,
|
||||
userId,
|
||||
username,
|
||||
displayName,
|
||||
roles,
|
||||
permissions,
|
||||
];
|
||||
}
|
||||
295
AINoval/lib/models/admin/admin_models.dart
Normal file
295
AINoval/lib/models/admin/admin_models.dart
Normal file
@@ -0,0 +1,295 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import '../../utils/date_time_parser.dart';
|
||||
|
||||
part 'admin_models.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class AdminDashboardStats extends Equatable {
|
||||
final int totalUsers;
|
||||
final int activeUsers;
|
||||
final int totalNovels;
|
||||
final int aiRequestsToday;
|
||||
final double creditsConsumed;
|
||||
final List<ChartData> userGrowthData;
|
||||
final List<ChartData> requestsData;
|
||||
final List<ActivityItem> recentActivities;
|
||||
|
||||
const AdminDashboardStats({
|
||||
required this.totalUsers,
|
||||
required this.activeUsers,
|
||||
required this.totalNovels,
|
||||
required this.aiRequestsToday,
|
||||
required this.creditsConsumed,
|
||||
required this.userGrowthData,
|
||||
required this.requestsData,
|
||||
required this.recentActivities,
|
||||
});
|
||||
|
||||
factory AdminDashboardStats.fromJson(Map<String, dynamic> json) =>
|
||||
_$AdminDashboardStatsFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AdminDashboardStatsToJson(this);
|
||||
|
||||
@override
|
||||
List<Object> get props => [
|
||||
totalUsers,
|
||||
activeUsers,
|
||||
totalNovels,
|
||||
aiRequestsToday,
|
||||
creditsConsumed,
|
||||
userGrowthData,
|
||||
requestsData,
|
||||
recentActivities,
|
||||
];
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class ChartData extends Equatable {
|
||||
final String label;
|
||||
final double value;
|
||||
final DateTime date;
|
||||
|
||||
const ChartData({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.date,
|
||||
});
|
||||
|
||||
|
||||
factory ChartData.fromJson(Map<String, dynamic> json) {
|
||||
return ChartData(
|
||||
label: json['label'] as String,
|
||||
value: (json['value'] as num).toDouble(),
|
||||
date: parseBackendDateTime(json['date']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$ChartDataToJson(this);
|
||||
|
||||
@override
|
||||
List<Object> get props => [label, value, date];
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class ActivityItem extends Equatable {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String userName;
|
||||
final String action;
|
||||
final String description;
|
||||
final DateTime timestamp;
|
||||
final String? metadata;
|
||||
|
||||
const ActivityItem({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.userName,
|
||||
required this.action,
|
||||
required this.description,
|
||||
required this.timestamp,
|
||||
this.metadata,
|
||||
});
|
||||
|
||||
factory ActivityItem.fromJson(Map<String, dynamic> json) {
|
||||
return ActivityItem(
|
||||
id: json['id'] as String,
|
||||
userId: json['userId'] as String,
|
||||
userName: json['userName'] as String,
|
||||
action: json['action'] as String,
|
||||
description: json['description'] as String,
|
||||
timestamp: parseBackendDateTime(json['timestamp']),
|
||||
metadata: json['metadata'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$ActivityItemToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
userId,
|
||||
userName,
|
||||
action,
|
||||
description,
|
||||
timestamp,
|
||||
metadata,
|
||||
];
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class AdminUser extends Equatable {
|
||||
final String id;
|
||||
final String username;
|
||||
final String email; // 后端可能返回 null,这里统一转换为空串
|
||||
final String? displayName;
|
||||
final String accountStatus;
|
||||
final int credits;
|
||||
final List<String> roles;
|
||||
final DateTime createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
const AdminUser({
|
||||
required this.id,
|
||||
required this.username,
|
||||
required this.email,
|
||||
this.displayName,
|
||||
required this.accountStatus,
|
||||
required this.credits,
|
||||
required this.roles,
|
||||
required this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
factory AdminUser.fromJson(Map<String, dynamic> json) {
|
||||
return AdminUser(
|
||||
id: json['id'] as String,
|
||||
username: json['username'] as String,
|
||||
email: (json['email'] as String?) ?? '',
|
||||
displayName: json['displayName'] as String?,
|
||||
accountStatus: json['accountStatus']?.toString() ?? 'ACTIVE',
|
||||
credits: (json['credits'] as num?)?.toInt() ?? 0,
|
||||
roles: (json['roles'] as List?)?.map((e) => e.toString()).toList() ?? [],
|
||||
createdAt: parseBackendDateTime(json['createdAt']),
|
||||
updatedAt: json['updatedAt'] != null ? parseBackendDateTime(json['updatedAt']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$AdminUserToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
username,
|
||||
email,
|
||||
displayName,
|
||||
accountStatus,
|
||||
credits,
|
||||
roles,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
];
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class AdminRole extends Equatable {
|
||||
final String? id;
|
||||
final String roleName;
|
||||
final String displayName;
|
||||
final String? description;
|
||||
final List<String> permissions;
|
||||
final bool enabled;
|
||||
final int priority;
|
||||
|
||||
const AdminRole({
|
||||
this.id,
|
||||
required this.roleName,
|
||||
required this.displayName,
|
||||
this.description,
|
||||
required this.permissions,
|
||||
required this.enabled,
|
||||
required this.priority,
|
||||
});
|
||||
|
||||
factory AdminRole.fromJson(Map<String, dynamic> json) =>
|
||||
_$AdminRoleFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AdminRoleToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
roleName,
|
||||
displayName,
|
||||
description,
|
||||
permissions,
|
||||
enabled,
|
||||
priority,
|
||||
];
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class AdminModelConfig extends Equatable {
|
||||
final String? id;
|
||||
final String provider;
|
||||
final String modelId;
|
||||
final String? displayName;
|
||||
final bool enabled;
|
||||
final List<String> enabledForFeatures;
|
||||
final double creditRateMultiplier;
|
||||
final int maxConcurrentRequests;
|
||||
final int dailyRequestLimit;
|
||||
final String? description;
|
||||
|
||||
const AdminModelConfig({
|
||||
this.id,
|
||||
required this.provider,
|
||||
required this.modelId,
|
||||
this.displayName,
|
||||
required this.enabled,
|
||||
required this.enabledForFeatures,
|
||||
required this.creditRateMultiplier,
|
||||
required this.maxConcurrentRequests,
|
||||
required this.dailyRequestLimit,
|
||||
this.description,
|
||||
});
|
||||
|
||||
factory AdminModelConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$AdminModelConfigFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AdminModelConfigToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
provider,
|
||||
modelId,
|
||||
displayName,
|
||||
enabled,
|
||||
enabledForFeatures,
|
||||
creditRateMultiplier,
|
||||
maxConcurrentRequests,
|
||||
dailyRequestLimit,
|
||||
description,
|
||||
];
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class AdminSystemConfig extends Equatable {
|
||||
final String id;
|
||||
final String configKey;
|
||||
final String configValue;
|
||||
final String? description;
|
||||
final String configType;
|
||||
final String? configGroup;
|
||||
final bool enabled;
|
||||
final bool readOnly;
|
||||
|
||||
const AdminSystemConfig({
|
||||
required this.id,
|
||||
required this.configKey,
|
||||
required this.configValue,
|
||||
this.description,
|
||||
required this.configType,
|
||||
this.configGroup,
|
||||
required this.enabled,
|
||||
required this.readOnly,
|
||||
});
|
||||
|
||||
factory AdminSystemConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$AdminSystemConfigFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AdminSystemConfigToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
configKey,
|
||||
configValue,
|
||||
description,
|
||||
configType,
|
||||
configGroup,
|
||||
enabled,
|
||||
readOnly,
|
||||
];
|
||||
}
|
||||
282
AINoval/lib/models/admin/admin_models.g.dart
Normal file
282
AINoval/lib/models/admin/admin_models.g.dart
Normal file
@@ -0,0 +1,282 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'admin_models.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
AdminDashboardStats _$AdminDashboardStatsFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'AdminDashboardStats',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = AdminDashboardStats(
|
||||
totalUsers: $checkedConvert('totalUsers', (v) => (v as num).toInt()),
|
||||
activeUsers:
|
||||
$checkedConvert('activeUsers', (v) => (v as num).toInt()),
|
||||
totalNovels:
|
||||
$checkedConvert('totalNovels', (v) => (v as num).toInt()),
|
||||
aiRequestsToday:
|
||||
$checkedConvert('aiRequestsToday', (v) => (v as num).toInt()),
|
||||
creditsConsumed:
|
||||
$checkedConvert('creditsConsumed', (v) => (v as num).toDouble()),
|
||||
userGrowthData: $checkedConvert(
|
||||
'userGrowthData',
|
||||
(v) => (v as List<dynamic>)
|
||||
.map((e) => ChartData.fromJson(e as Map<String, dynamic>))
|
||||
.toList()),
|
||||
requestsData: $checkedConvert(
|
||||
'requestsData',
|
||||
(v) => (v as List<dynamic>)
|
||||
.map((e) => ChartData.fromJson(e as Map<String, dynamic>))
|
||||
.toList()),
|
||||
recentActivities: $checkedConvert(
|
||||
'recentActivities',
|
||||
(v) => (v as List<dynamic>)
|
||||
.map((e) => ActivityItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList()),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AdminDashboardStatsToJson(
|
||||
AdminDashboardStats instance) =>
|
||||
<String, dynamic>{
|
||||
'totalUsers': instance.totalUsers,
|
||||
'activeUsers': instance.activeUsers,
|
||||
'totalNovels': instance.totalNovels,
|
||||
'aiRequestsToday': instance.aiRequestsToday,
|
||||
'creditsConsumed': instance.creditsConsumed,
|
||||
'userGrowthData': instance.userGrowthData.map((e) => e.toJson()).toList(),
|
||||
'requestsData': instance.requestsData.map((e) => e.toJson()).toList(),
|
||||
'recentActivities':
|
||||
instance.recentActivities.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
ChartData _$ChartDataFromJson(Map<String, dynamic> json) => $checkedCreate(
|
||||
'ChartData',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = ChartData(
|
||||
label: $checkedConvert('label', (v) => v as String),
|
||||
value: $checkedConvert('value', (v) => (v as num).toDouble()),
|
||||
date: $checkedConvert('date', (v) => DateTime.parse(v as String)),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ChartDataToJson(ChartData instance) => <String, dynamic>{
|
||||
'label': instance.label,
|
||||
'value': instance.value,
|
||||
'date': instance.date.toIso8601String(),
|
||||
};
|
||||
|
||||
ActivityItem _$ActivityItemFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'ActivityItem',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = ActivityItem(
|
||||
id: $checkedConvert('id', (v) => v as String),
|
||||
userId: $checkedConvert('userId', (v) => v as String),
|
||||
userName: $checkedConvert('userName', (v) => v as String),
|
||||
action: $checkedConvert('action', (v) => v as String),
|
||||
description: $checkedConvert('description', (v) => v as String),
|
||||
timestamp:
|
||||
$checkedConvert('timestamp', (v) => DateTime.parse(v as String)),
|
||||
metadata: $checkedConvert('metadata', (v) => v as String?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ActivityItemToJson(ActivityItem instance) {
|
||||
final val = <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'userId': instance.userId,
|
||||
'userName': instance.userName,
|
||||
'action': instance.action,
|
||||
'description': instance.description,
|
||||
'timestamp': instance.timestamp.toIso8601String(),
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('metadata', instance.metadata);
|
||||
return val;
|
||||
}
|
||||
|
||||
AdminUser _$AdminUserFromJson(Map<String, dynamic> json) => $checkedCreate(
|
||||
'AdminUser',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = AdminUser(
|
||||
id: $checkedConvert('id', (v) => v as String),
|
||||
username: $checkedConvert('username', (v) => v as String),
|
||||
email: $checkedConvert('email', (v) => v as String),
|
||||
displayName: $checkedConvert('displayName', (v) => v as String?),
|
||||
accountStatus: $checkedConvert('accountStatus', (v) => v as String),
|
||||
credits: $checkedConvert('credits', (v) => (v as num).toInt()),
|
||||
roles: $checkedConvert('roles',
|
||||
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
|
||||
createdAt:
|
||||
$checkedConvert('createdAt', (v) => DateTime.parse(v as String)),
|
||||
updatedAt: $checkedConvert('updatedAt',
|
||||
(v) => v == null ? null : DateTime.parse(v as String)),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AdminUserToJson(AdminUser instance) {
|
||||
final val = <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'username': instance.username,
|
||||
'email': instance.email,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('displayName', instance.displayName);
|
||||
val['accountStatus'] = instance.accountStatus;
|
||||
val['credits'] = instance.credits;
|
||||
val['roles'] = instance.roles;
|
||||
val['createdAt'] = instance.createdAt.toIso8601String();
|
||||
writeNotNull('updatedAt', instance.updatedAt?.toIso8601String());
|
||||
return val;
|
||||
}
|
||||
|
||||
AdminRole _$AdminRoleFromJson(Map<String, dynamic> json) => $checkedCreate(
|
||||
'AdminRole',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = AdminRole(
|
||||
id: $checkedConvert('id', (v) => v as String?),
|
||||
roleName: $checkedConvert('roleName', (v) => v as String),
|
||||
displayName: $checkedConvert('displayName', (v) => v as String),
|
||||
description: $checkedConvert('description', (v) => v as String?),
|
||||
permissions: $checkedConvert('permissions',
|
||||
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
|
||||
enabled: $checkedConvert('enabled', (v) => v as bool),
|
||||
priority: $checkedConvert('priority', (v) => (v as num).toInt()),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AdminRoleToJson(AdminRole instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('id', instance.id);
|
||||
val['roleName'] = instance.roleName;
|
||||
val['displayName'] = instance.displayName;
|
||||
writeNotNull('description', instance.description);
|
||||
val['permissions'] = instance.permissions;
|
||||
val['enabled'] = instance.enabled;
|
||||
val['priority'] = instance.priority;
|
||||
return val;
|
||||
}
|
||||
|
||||
AdminModelConfig _$AdminModelConfigFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'AdminModelConfig',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = AdminModelConfig(
|
||||
id: $checkedConvert('id', (v) => v as String?),
|
||||
provider: $checkedConvert('provider', (v) => v as String),
|
||||
modelId: $checkedConvert('modelId', (v) => v as String),
|
||||
displayName: $checkedConvert('displayName', (v) => v as String?),
|
||||
enabled: $checkedConvert('enabled', (v) => v as bool),
|
||||
enabledForFeatures: $checkedConvert('enabledForFeatures',
|
||||
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
|
||||
creditRateMultiplier: $checkedConvert(
|
||||
'creditRateMultiplier', (v) => (v as num).toDouble()),
|
||||
maxConcurrentRequests: $checkedConvert(
|
||||
'maxConcurrentRequests', (v) => (v as num).toInt()),
|
||||
dailyRequestLimit:
|
||||
$checkedConvert('dailyRequestLimit', (v) => (v as num).toInt()),
|
||||
description: $checkedConvert('description', (v) => v as String?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AdminModelConfigToJson(AdminModelConfig instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('id', instance.id);
|
||||
val['provider'] = instance.provider;
|
||||
val['modelId'] = instance.modelId;
|
||||
writeNotNull('displayName', instance.displayName);
|
||||
val['enabled'] = instance.enabled;
|
||||
val['enabledForFeatures'] = instance.enabledForFeatures;
|
||||
val['creditRateMultiplier'] = instance.creditRateMultiplier;
|
||||
val['maxConcurrentRequests'] = instance.maxConcurrentRequests;
|
||||
val['dailyRequestLimit'] = instance.dailyRequestLimit;
|
||||
writeNotNull('description', instance.description);
|
||||
return val;
|
||||
}
|
||||
|
||||
AdminSystemConfig _$AdminSystemConfigFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'AdminSystemConfig',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = AdminSystemConfig(
|
||||
id: $checkedConvert('id', (v) => v as String),
|
||||
configKey: $checkedConvert('configKey', (v) => v as String),
|
||||
configValue: $checkedConvert('configValue', (v) => v as String),
|
||||
description: $checkedConvert('description', (v) => v as String?),
|
||||
configType: $checkedConvert('configType', (v) => v as String),
|
||||
configGroup: $checkedConvert('configGroup', (v) => v as String?),
|
||||
enabled: $checkedConvert('enabled', (v) => v as bool),
|
||||
readOnly: $checkedConvert('readOnly', (v) => v as bool),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AdminSystemConfigToJson(AdminSystemConfig instance) {
|
||||
final val = <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'configKey': instance.configKey,
|
||||
'configValue': instance.configValue,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('description', instance.description);
|
||||
val['configType'] = instance.configType;
|
||||
writeNotNull('configGroup', instance.configGroup);
|
||||
val['enabled'] = instance.enabled;
|
||||
val['readOnly'] = instance.readOnly;
|
||||
return val;
|
||||
}
|
||||
54
AINoval/lib/models/admin/billing_models.dart
Normal file
54
AINoval/lib/models/admin/billing_models.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
class CreditTransactionModel {
|
||||
final String traceId;
|
||||
final String? userId;
|
||||
final String? provider;
|
||||
final String? modelId;
|
||||
final String? featureType;
|
||||
final int? inputTokens;
|
||||
final int? outputTokens;
|
||||
final int? creditsDeducted;
|
||||
final String status; // PENDING, DEDUCTED, FAILED, COMPENSATED
|
||||
final String? errorMessage;
|
||||
final String? reversalOfTraceId;
|
||||
final String? operatorUserId;
|
||||
final String? auditNote;
|
||||
final String? createdAt; // ISO8601 from backend
|
||||
|
||||
CreditTransactionModel({
|
||||
required this.traceId,
|
||||
required this.status,
|
||||
this.userId,
|
||||
this.provider,
|
||||
this.modelId,
|
||||
this.featureType,
|
||||
this.inputTokens,
|
||||
this.outputTokens,
|
||||
this.creditsDeducted,
|
||||
this.errorMessage,
|
||||
this.reversalOfTraceId,
|
||||
this.operatorUserId,
|
||||
this.auditNote,
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
factory CreditTransactionModel.fromJson(Map<String, dynamic> json) {
|
||||
return CreditTransactionModel(
|
||||
traceId: (json['traceId'] ?? '').toString(),
|
||||
userId: json['userId']?.toString(),
|
||||
provider: json['provider']?.toString(),
|
||||
modelId: json['modelId']?.toString(),
|
||||
featureType: json['featureType']?.toString(),
|
||||
inputTokens: json['inputTokens'] is int ? json['inputTokens'] as int : int.tryParse('${json['inputTokens'] ?? ''}'),
|
||||
outputTokens: json['outputTokens'] is int ? json['outputTokens'] as int : int.tryParse('${json['outputTokens'] ?? ''}'),
|
||||
creditsDeducted: json['creditsDeducted'] is int ? json['creditsDeducted'] as int : int.tryParse('${json['creditsDeducted'] ?? ''}'),
|
||||
status: (json['status'] ?? '').toString(),
|
||||
errorMessage: json['errorMessage']?.toString(),
|
||||
reversalOfTraceId: json['reversalOfTraceId']?.toString(),
|
||||
operatorUserId: json['operatorUserId']?.toString(),
|
||||
auditNote: json['auditNote']?.toString(),
|
||||
createdAt: json['createdAt']?.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
702
AINoval/lib/models/admin/llm_observability_models.dart
Normal file
702
AINoval/lib/models/admin/llm_observability_models.dart
Normal file
@@ -0,0 +1,702 @@
|
||||
/// LLM可观测性相关数据模型
|
||||
/// 用于管理后台查看和分析大模型调用日志
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import '../../utils/date_time_parser.dart';
|
||||
|
||||
part 'llm_observability_models.g.dart';
|
||||
|
||||
/// 自定义时间戳转换器
|
||||
class TimestampConverter implements JsonConverter<DateTime, dynamic> {
|
||||
const TimestampConverter();
|
||||
|
||||
@override
|
||||
DateTime fromJson(dynamic timestamp) {
|
||||
return parseBackendDateTime(timestamp);
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic toJson(DateTime timestamp) {
|
||||
return timestamp.toIso8601String();
|
||||
}
|
||||
}
|
||||
|
||||
/// LLM调用日志
|
||||
@JsonSerializable()
|
||||
class LLMTrace extends Equatable {
|
||||
final String id;
|
||||
final String traceId;
|
||||
final String provider;
|
||||
final String model;
|
||||
final String? userId;
|
||||
final String? sessionId;
|
||||
@JsonKey(name: 'createdAt')
|
||||
@TimestampConverter()
|
||||
final DateTime timestamp;
|
||||
|
||||
// 请求信息
|
||||
final LLMRequest request;
|
||||
|
||||
// 响应信息
|
||||
final LLMResponse? response;
|
||||
|
||||
// 性能指标
|
||||
final LLMPerformanceMetrics? performance;
|
||||
|
||||
// 错误信息
|
||||
final LLMError? error;
|
||||
|
||||
// 工具调用
|
||||
final List<LLMToolCall>? toolCalls;
|
||||
|
||||
// 元数据
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
// 状态
|
||||
@JsonKey(defaultValue: LLMTraceStatus.pending)
|
||||
final LLMTraceStatus status;
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool isStreaming;
|
||||
|
||||
const LLMTrace({
|
||||
required this.id,
|
||||
required this.traceId,
|
||||
required this.provider,
|
||||
required this.model,
|
||||
this.userId,
|
||||
this.sessionId,
|
||||
required this.timestamp,
|
||||
required this.request,
|
||||
this.response,
|
||||
this.performance,
|
||||
this.error,
|
||||
this.toolCalls,
|
||||
this.metadata,
|
||||
this.status = LLMTraceStatus.pending,
|
||||
this.isStreaming = false,
|
||||
});
|
||||
|
||||
factory LLMTrace.fromJson(Map<String, dynamic> json) => _$LLMTraceFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LLMTraceToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, traceId, provider, model, userId, sessionId, timestamp, request, response, performance, error, toolCalls, metadata, status, isStreaming];
|
||||
}
|
||||
|
||||
/// LLM请求信息
|
||||
@JsonSerializable()
|
||||
class LLMRequest extends Equatable {
|
||||
final List<LLMMessage>? messages;
|
||||
|
||||
// 模型参数
|
||||
final double? temperature;
|
||||
final double? topP;
|
||||
final int? topK;
|
||||
final int? maxTokens;
|
||||
final int? seed;
|
||||
|
||||
// 工具调用
|
||||
final List<LLMTool>? tools;
|
||||
final String? toolChoice;
|
||||
|
||||
// 格式设置
|
||||
final String? responseFormat;
|
||||
|
||||
// 其他参数
|
||||
final Map<String, dynamic>? additionalParameters;
|
||||
|
||||
const LLMRequest({
|
||||
this.messages,
|
||||
this.temperature,
|
||||
this.topP,
|
||||
this.topK,
|
||||
this.maxTokens,
|
||||
this.seed,
|
||||
this.tools,
|
||||
this.toolChoice,
|
||||
this.responseFormat,
|
||||
this.additionalParameters,
|
||||
});
|
||||
|
||||
factory LLMRequest.fromJson(Map<String, dynamic> json) => _$LLMRequestFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LLMRequestToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [messages, temperature, topP, topK, maxTokens, seed, tools, toolChoice, responseFormat, additionalParameters];
|
||||
}
|
||||
|
||||
/// LLM响应信息
|
||||
@JsonSerializable()
|
||||
class LLMResponse extends Equatable {
|
||||
final String? id;
|
||||
final String? content;
|
||||
|
||||
// Token使用情况
|
||||
final LLMTokenUsage? tokenUsage;
|
||||
|
||||
// 完成原因
|
||||
final String? finishReason;
|
||||
|
||||
// 工具调用结果
|
||||
final List<LLMToolCallResult>? toolCallResults;
|
||||
|
||||
// 元数据
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
// 流式数据
|
||||
final List<String>? streamChunks;
|
||||
|
||||
const LLMResponse({
|
||||
this.id,
|
||||
this.content,
|
||||
this.tokenUsage,
|
||||
this.finishReason,
|
||||
this.toolCallResults,
|
||||
this.metadata,
|
||||
this.streamChunks,
|
||||
});
|
||||
|
||||
factory LLMResponse.fromJson(Map<String, dynamic> json) => _$LLMResponseFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LLMResponseToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, content, tokenUsage, finishReason, toolCallResults, metadata, streamChunks];
|
||||
}
|
||||
|
||||
/// LLM消息
|
||||
@JsonSerializable()
|
||||
class LLMMessage extends Equatable {
|
||||
final String role;
|
||||
final String? content;
|
||||
final String? name;
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
const LLMMessage({
|
||||
required this.role,
|
||||
this.content,
|
||||
this.name,
|
||||
this.metadata,
|
||||
});
|
||||
|
||||
factory LLMMessage.fromJson(Map<String, dynamic> json) => _$LLMMessageFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LLMMessageToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [role, content, name, metadata];
|
||||
}
|
||||
|
||||
/// LLM工具定义
|
||||
@JsonSerializable()
|
||||
class LLMTool extends Equatable {
|
||||
final String name;
|
||||
final String? description;
|
||||
final Map<String, dynamic>? parameters;
|
||||
|
||||
const LLMTool({
|
||||
required this.name,
|
||||
this.description,
|
||||
this.parameters,
|
||||
});
|
||||
|
||||
factory LLMTool.fromJson(Map<String, dynamic> json) => _$LLMToolFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LLMToolToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, description, parameters];
|
||||
}
|
||||
|
||||
/// LLM工具调用
|
||||
@JsonSerializable()
|
||||
class LLMToolCall extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final Map<String, dynamic>? arguments;
|
||||
final DateTime? timestamp;
|
||||
|
||||
const LLMToolCall({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.arguments,
|
||||
this.timestamp,
|
||||
});
|
||||
|
||||
factory LLMToolCall.fromJson(Map<String, dynamic> json) => _$LLMToolCallFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LLMToolCallToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, name, arguments, timestamp];
|
||||
}
|
||||
|
||||
/// LLM工具调用结果
|
||||
@JsonSerializable()
|
||||
class LLMToolCallResult extends Equatable {
|
||||
final String toolCallId;
|
||||
final String? result;
|
||||
final LLMError? error;
|
||||
|
||||
const LLMToolCallResult({
|
||||
required this.toolCallId,
|
||||
this.result,
|
||||
this.error,
|
||||
});
|
||||
|
||||
factory LLMToolCallResult.fromJson(Map<String, dynamic> json) => _$LLMToolCallResultFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LLMToolCallResultToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [toolCallId, result, error];
|
||||
}
|
||||
|
||||
/// Token使用情况
|
||||
@JsonSerializable()
|
||||
class LLMTokenUsage extends Equatable {
|
||||
final int? promptTokens;
|
||||
final int? completionTokens;
|
||||
final int? totalTokens;
|
||||
|
||||
// 详细分解
|
||||
final int? inputTokens;
|
||||
final int? outputTokens;
|
||||
final int? reasoningTokens;
|
||||
final int? cachedTokens;
|
||||
|
||||
const LLMTokenUsage({
|
||||
this.promptTokens,
|
||||
this.completionTokens,
|
||||
this.totalTokens,
|
||||
this.inputTokens,
|
||||
this.outputTokens,
|
||||
this.reasoningTokens,
|
||||
this.cachedTokens,
|
||||
});
|
||||
|
||||
factory LLMTokenUsage.fromJson(Map<String, dynamic> json) => _$LLMTokenUsageFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LLMTokenUsageToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [promptTokens, completionTokens, totalTokens, inputTokens, outputTokens, reasoningTokens, cachedTokens];
|
||||
}
|
||||
|
||||
/// 性能指标
|
||||
@JsonSerializable()
|
||||
class LLMPerformanceMetrics extends Equatable {
|
||||
final int? requestLatencyMs;
|
||||
final int? firstTokenLatencyMs;
|
||||
final int? totalDurationMs;
|
||||
|
||||
// 吞吐量
|
||||
final double? tokensPerSecond;
|
||||
final double? charactersPerSecond;
|
||||
|
||||
// 队列时间
|
||||
final int? queueTimeMs;
|
||||
final int? processingTimeMs;
|
||||
|
||||
const LLMPerformanceMetrics({
|
||||
this.requestLatencyMs,
|
||||
this.firstTokenLatencyMs,
|
||||
this.totalDurationMs,
|
||||
this.tokensPerSecond,
|
||||
this.charactersPerSecond,
|
||||
this.queueTimeMs,
|
||||
this.processingTimeMs,
|
||||
});
|
||||
|
||||
factory LLMPerformanceMetrics.fromJson(Map<String, dynamic> json) => _$LLMPerformanceMetricsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LLMPerformanceMetricsToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [requestLatencyMs, firstTokenLatencyMs, totalDurationMs, tokensPerSecond, charactersPerSecond, queueTimeMs, processingTimeMs];
|
||||
}
|
||||
|
||||
/// 错误信息
|
||||
@JsonSerializable()
|
||||
class LLMError extends Equatable {
|
||||
final String? type;
|
||||
final String? message;
|
||||
final String? code;
|
||||
final String? stackTrace;
|
||||
final Map<String, dynamic>? details;
|
||||
|
||||
const LLMError({
|
||||
this.type,
|
||||
this.message,
|
||||
this.code,
|
||||
this.stackTrace,
|
||||
this.details,
|
||||
});
|
||||
|
||||
factory LLMError.fromJson(Map<String, dynamic> json) => _$LLMErrorFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LLMErrorToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [type, message, code, stackTrace, details];
|
||||
}
|
||||
|
||||
/// LLM调用状态
|
||||
enum LLMTraceStatus {
|
||||
@JsonValue('pending')
|
||||
pending,
|
||||
@JsonValue('success')
|
||||
success,
|
||||
@JsonValue('error')
|
||||
error,
|
||||
@JsonValue('timeout')
|
||||
timeout,
|
||||
@JsonValue('cancelled')
|
||||
cancelled,
|
||||
}
|
||||
|
||||
/// 统计信息基类
|
||||
@JsonSerializable()
|
||||
class LLMStatistics extends Equatable {
|
||||
final int totalCalls;
|
||||
final int successfulCalls;
|
||||
final int failedCalls;
|
||||
final double successRate;
|
||||
final double averageLatency;
|
||||
final int totalTokens;
|
||||
|
||||
// 时间范围
|
||||
final DateTime? startTime;
|
||||
final DateTime? endTime;
|
||||
|
||||
// 详细统计
|
||||
final Map<String, dynamic>? details;
|
||||
|
||||
const LLMStatistics({
|
||||
required this.totalCalls,
|
||||
required this.successfulCalls,
|
||||
required this.failedCalls,
|
||||
required this.successRate,
|
||||
required this.averageLatency,
|
||||
required this.totalTokens,
|
||||
this.startTime,
|
||||
this.endTime,
|
||||
this.details,
|
||||
});
|
||||
|
||||
factory LLMStatistics.fromJson(Map<String, dynamic> json) => _$LLMStatisticsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LLMStatisticsToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [totalCalls, successfulCalls, failedCalls, successRate, averageLatency, totalTokens, startTime, endTime, details];
|
||||
}
|
||||
|
||||
/// 提供商统计
|
||||
@JsonSerializable()
|
||||
class ProviderStatistics extends Equatable {
|
||||
final String provider;
|
||||
final LLMStatistics statistics;
|
||||
final List<ModelStatistics> models;
|
||||
|
||||
const ProviderStatistics({
|
||||
required this.provider,
|
||||
required this.statistics,
|
||||
required this.models,
|
||||
});
|
||||
|
||||
factory ProviderStatistics.fromJson(Map<String, dynamic> json) => _$ProviderStatisticsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ProviderStatisticsToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [provider, statistics, models];
|
||||
}
|
||||
|
||||
/// 模型统计
|
||||
@JsonSerializable()
|
||||
class ModelStatistics extends Equatable {
|
||||
final String modelName;
|
||||
final String provider;
|
||||
final LLMStatistics statistics;
|
||||
|
||||
const ModelStatistics({
|
||||
required this.modelName,
|
||||
required this.provider,
|
||||
required this.statistics,
|
||||
});
|
||||
|
||||
factory ModelStatistics.fromJson(Map<String, dynamic> json) => _$ModelStatisticsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ModelStatisticsToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [modelName, provider, statistics];
|
||||
}
|
||||
|
||||
/// 用户统计
|
||||
@JsonSerializable()
|
||||
class UserStatistics extends Equatable {
|
||||
final String userId;
|
||||
final String? username;
|
||||
final LLMStatistics statistics;
|
||||
final List<String> topModels;
|
||||
final List<String> topProviders;
|
||||
|
||||
const UserStatistics({
|
||||
required this.userId,
|
||||
this.username,
|
||||
required this.statistics,
|
||||
required this.topModels,
|
||||
required this.topProviders,
|
||||
});
|
||||
|
||||
factory UserStatistics.fromJson(Map<String, dynamic> json) => _$UserStatisticsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$UserStatisticsToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [userId, username, statistics, topModels, topProviders];
|
||||
}
|
||||
|
||||
/// 错误统计
|
||||
@JsonSerializable()
|
||||
class ErrorStatistics extends Equatable {
|
||||
final String errorType;
|
||||
final int count;
|
||||
final double percentage;
|
||||
final List<String> topErrorMessages;
|
||||
final List<String> affectedModels;
|
||||
|
||||
const ErrorStatistics({
|
||||
required this.errorType,
|
||||
required this.count,
|
||||
required this.percentage,
|
||||
required this.topErrorMessages,
|
||||
required this.affectedModels,
|
||||
});
|
||||
|
||||
factory ErrorStatistics.fromJson(Map<String, dynamic> json) => _$ErrorStatisticsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ErrorStatisticsToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [errorType, count, percentage, topErrorMessages, affectedModels];
|
||||
}
|
||||
|
||||
/// 性能统计
|
||||
@JsonSerializable()
|
||||
class PerformanceStatistics extends Equatable {
|
||||
final double averageLatency;
|
||||
final double medianLatency;
|
||||
final double p95Latency;
|
||||
final double p99Latency;
|
||||
final double averageThroughput;
|
||||
|
||||
// 按时间分组的统计
|
||||
final List<TimeBasedMetric> latencyTrends;
|
||||
final List<TimeBasedMetric> throughputTrends;
|
||||
|
||||
const PerformanceStatistics({
|
||||
required this.averageLatency,
|
||||
required this.medianLatency,
|
||||
required this.p95Latency,
|
||||
required this.p99Latency,
|
||||
required this.averageThroughput,
|
||||
required this.latencyTrends,
|
||||
required this.throughputTrends,
|
||||
});
|
||||
|
||||
factory PerformanceStatistics.fromJson(Map<String, dynamic> json) => _$PerformanceStatisticsFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$PerformanceStatisticsToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [averageLatency, medianLatency, p95Latency, p99Latency, averageThroughput, latencyTrends, throughputTrends];
|
||||
}
|
||||
|
||||
/// 基于时间的指标
|
||||
@JsonSerializable()
|
||||
class TimeBasedMetric extends Equatable {
|
||||
final DateTime timestamp;
|
||||
final double value;
|
||||
final String? label;
|
||||
|
||||
const TimeBasedMetric({
|
||||
required this.timestamp,
|
||||
required this.value,
|
||||
this.label,
|
||||
});
|
||||
|
||||
factory TimeBasedMetric.fromJson(Map<String, dynamic> json) => _$TimeBasedMetricFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$TimeBasedMetricToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [timestamp, value, label];
|
||||
}
|
||||
|
||||
/// 系统健康状态
|
||||
@JsonSerializable()
|
||||
class SystemHealthStatus extends Equatable {
|
||||
@JsonKey(defaultValue: HealthStatus.healthy)
|
||||
final HealthStatus status;
|
||||
final Map<String, ComponentHealth> components;
|
||||
final String? message;
|
||||
final DateTime? lastChecked;
|
||||
|
||||
const SystemHealthStatus({
|
||||
this.status = HealthStatus.healthy,
|
||||
required this.components,
|
||||
this.message,
|
||||
this.lastChecked,
|
||||
});
|
||||
|
||||
factory SystemHealthStatus.fromJson(Map<String, dynamic> json) => _$SystemHealthStatusFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$SystemHealthStatusToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, components, message, lastChecked];
|
||||
}
|
||||
|
||||
/// 组件健康状态
|
||||
@JsonSerializable()
|
||||
class ComponentHealth extends Equatable {
|
||||
@JsonKey(defaultValue: HealthStatus.healthy)
|
||||
final HealthStatus status;
|
||||
final String? message;
|
||||
final Map<String, dynamic>? metrics;
|
||||
|
||||
const ComponentHealth({
|
||||
this.status = HealthStatus.healthy,
|
||||
this.message,
|
||||
this.metrics,
|
||||
});
|
||||
|
||||
factory ComponentHealth.fromJson(Map<String, dynamic> json) => _$ComponentHealthFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$ComponentHealthToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, message, metrics];
|
||||
}
|
||||
|
||||
/// 健康状态枚举
|
||||
enum HealthStatus {
|
||||
@JsonValue('healthy')
|
||||
healthy,
|
||||
@JsonValue('degraded')
|
||||
degraded,
|
||||
@JsonValue('unhealthy')
|
||||
unhealthy,
|
||||
@JsonValue('unknown')
|
||||
unknown,
|
||||
}
|
||||
|
||||
/// LLM日志搜索条件
|
||||
@JsonSerializable()
|
||||
class LLMTraceSearchCriteria extends Equatable {
|
||||
final String? userId;
|
||||
final String? provider;
|
||||
final String? model;
|
||||
final String? sessionId;
|
||||
final bool? hasError;
|
||||
final LLMTraceStatus? status;
|
||||
final DateTime? startTime;
|
||||
final DateTime? endTime;
|
||||
|
||||
// 分页
|
||||
@JsonKey(defaultValue: 0)
|
||||
final int page;
|
||||
@JsonKey(defaultValue: 20)
|
||||
final int size;
|
||||
@JsonKey(defaultValue: 'timestamp')
|
||||
final String sortBy;
|
||||
@JsonKey(defaultValue: 'desc')
|
||||
final String sortDir;
|
||||
|
||||
const LLMTraceSearchCriteria({
|
||||
this.userId,
|
||||
this.provider,
|
||||
this.model,
|
||||
this.sessionId,
|
||||
this.hasError,
|
||||
this.status,
|
||||
this.startTime,
|
||||
this.endTime,
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
this.sortBy = 'timestamp',
|
||||
this.sortDir = 'desc',
|
||||
});
|
||||
|
||||
factory LLMTraceSearchCriteria.fromJson(Map<String, dynamic> json) => _$LLMTraceSearchCriteriaFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$LLMTraceSearchCriteriaToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [userId, provider, model, sessionId, hasError, status, startTime, endTime, page, size, sortBy, sortDir];
|
||||
}
|
||||
|
||||
/// API响应包装类
|
||||
@JsonSerializable(genericArgumentFactories: true)
|
||||
class ApiResponse<T> extends Equatable {
|
||||
final bool success;
|
||||
final String? message;
|
||||
final T? data;
|
||||
final String? error;
|
||||
|
||||
const ApiResponse({
|
||||
required this.success,
|
||||
this.message,
|
||||
this.data,
|
||||
this.error,
|
||||
});
|
||||
|
||||
factory ApiResponse.fromJson(Map<String, dynamic> json, T Function(Object? json) fromJsonT) =>
|
||||
_$ApiResponseFromJson(json, fromJsonT);
|
||||
Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
|
||||
_$ApiResponseToJson(this, toJsonT);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [success, message, data, error];
|
||||
}
|
||||
|
||||
/// 分页响应
|
||||
@JsonSerializable(genericArgumentFactories: true)
|
||||
class PagedResponse<T> extends Equatable {
|
||||
final List<T> content;
|
||||
final int page;
|
||||
final int size;
|
||||
final int totalElements;
|
||||
final int totalPages;
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool first;
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool last;
|
||||
|
||||
const PagedResponse({
|
||||
required this.content,
|
||||
required this.page,
|
||||
required this.size,
|
||||
required this.totalElements,
|
||||
required this.totalPages,
|
||||
this.first = false,
|
||||
this.last = false,
|
||||
});
|
||||
|
||||
factory PagedResponse.fromJson(Map<String, dynamic> json, T Function(Object? json) fromJsonT) =>
|
||||
_$PagedResponseFromJson(json, fromJsonT);
|
||||
Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
|
||||
_$PagedResponseToJson(this, toJsonT);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [content, page, size, totalElements, totalPages, first, last];
|
||||
}
|
||||
|
||||
/// 游标分页响应
|
||||
@JsonSerializable(genericArgumentFactories: true)
|
||||
class CursorPageResponse<T> extends Equatable {
|
||||
final List<T> items;
|
||||
final String? nextCursor;
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool hasMore;
|
||||
|
||||
const CursorPageResponse({
|
||||
required this.items,
|
||||
this.nextCursor,
|
||||
this.hasMore = false,
|
||||
});
|
||||
|
||||
factory CursorPageResponse.fromJson(Map<String, dynamic> json, T Function(Object? json) fromJsonT) =>
|
||||
_$CursorPageResponseFromJson(json, fromJsonT);
|
||||
Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
|
||||
_$CursorPageResponseToJson(this, toJsonT);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [items, nextCursor, hasMore];
|
||||
}
|
||||
951
AINoval/lib/models/admin/llm_observability_models.g.dart
Normal file
951
AINoval/lib/models/admin/llm_observability_models.g.dart
Normal file
@@ -0,0 +1,951 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'llm_observability_models.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
LLMTrace _$LLMTraceFromJson(Map<String, dynamic> json) => $checkedCreate(
|
||||
'LLMTrace',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = LLMTrace(
|
||||
id: $checkedConvert('id', (v) => v as String),
|
||||
traceId: $checkedConvert('traceId', (v) => v as String),
|
||||
provider: $checkedConvert('provider', (v) => v as String),
|
||||
model: $checkedConvert('model', (v) => v as String),
|
||||
userId: $checkedConvert('userId', (v) => v as String?),
|
||||
sessionId: $checkedConvert('sessionId', (v) => v as String?),
|
||||
timestamp: $checkedConvert(
|
||||
'createdAt', (v) => const TimestampConverter().fromJson(v)),
|
||||
request: $checkedConvert(
|
||||
'request', (v) => LLMRequest.fromJson(v as Map<String, dynamic>)),
|
||||
response: $checkedConvert(
|
||||
'response',
|
||||
(v) => v == null
|
||||
? null
|
||||
: LLMResponse.fromJson(v as Map<String, dynamic>)),
|
||||
performance: $checkedConvert(
|
||||
'performance',
|
||||
(v) => v == null
|
||||
? null
|
||||
: LLMPerformanceMetrics.fromJson(v as Map<String, dynamic>)),
|
||||
error: $checkedConvert(
|
||||
'error',
|
||||
(v) => v == null
|
||||
? null
|
||||
: LLMError.fromJson(v as Map<String, dynamic>)),
|
||||
toolCalls: $checkedConvert(
|
||||
'toolCalls',
|
||||
(v) => (v as List<dynamic>?)
|
||||
?.map((e) => LLMToolCall.fromJson(e as Map<String, dynamic>))
|
||||
.toList()),
|
||||
metadata:
|
||||
$checkedConvert('metadata', (v) => v as Map<String, dynamic>?),
|
||||
status: $checkedConvert(
|
||||
'status',
|
||||
(v) =>
|
||||
$enumDecodeNullable(_$LLMTraceStatusEnumMap, v) ??
|
||||
LLMTraceStatus.pending),
|
||||
isStreaming:
|
||||
$checkedConvert('isStreaming', (v) => v as bool? ?? false),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
fieldKeyMap: const {'timestamp': 'createdAt'},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LLMTraceToJson(LLMTrace instance) {
|
||||
final val = <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'traceId': instance.traceId,
|
||||
'provider': instance.provider,
|
||||
'model': instance.model,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('userId', instance.userId);
|
||||
writeNotNull('sessionId', instance.sessionId);
|
||||
writeNotNull(
|
||||
'createdAt', const TimestampConverter().toJson(instance.timestamp));
|
||||
val['request'] = instance.request.toJson();
|
||||
writeNotNull('response', instance.response?.toJson());
|
||||
writeNotNull('performance', instance.performance?.toJson());
|
||||
writeNotNull('error', instance.error?.toJson());
|
||||
writeNotNull(
|
||||
'toolCalls', instance.toolCalls?.map((e) => e.toJson()).toList());
|
||||
writeNotNull('metadata', instance.metadata);
|
||||
val['status'] = _$LLMTraceStatusEnumMap[instance.status]!;
|
||||
val['isStreaming'] = instance.isStreaming;
|
||||
return val;
|
||||
}
|
||||
|
||||
const _$LLMTraceStatusEnumMap = {
|
||||
LLMTraceStatus.pending: 'pending',
|
||||
LLMTraceStatus.success: 'success',
|
||||
LLMTraceStatus.error: 'error',
|
||||
LLMTraceStatus.timeout: 'timeout',
|
||||
LLMTraceStatus.cancelled: 'cancelled',
|
||||
};
|
||||
|
||||
LLMRequest _$LLMRequestFromJson(Map<String, dynamic> json) => $checkedCreate(
|
||||
'LLMRequest',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = LLMRequest(
|
||||
messages: $checkedConvert(
|
||||
'messages',
|
||||
(v) => (v as List<dynamic>?)
|
||||
?.map((e) => LLMMessage.fromJson(e as Map<String, dynamic>))
|
||||
.toList()),
|
||||
temperature:
|
||||
$checkedConvert('temperature', (v) => (v as num?)?.toDouble()),
|
||||
topP: $checkedConvert('topP', (v) => (v as num?)?.toDouble()),
|
||||
topK: $checkedConvert('topK', (v) => (v as num?)?.toInt()),
|
||||
maxTokens: $checkedConvert('maxTokens', (v) => (v as num?)?.toInt()),
|
||||
seed: $checkedConvert('seed', (v) => (v as num?)?.toInt()),
|
||||
tools: $checkedConvert(
|
||||
'tools',
|
||||
(v) => (v as List<dynamic>?)
|
||||
?.map((e) => LLMTool.fromJson(e as Map<String, dynamic>))
|
||||
.toList()),
|
||||
toolChoice: $checkedConvert('toolChoice', (v) => v as String?),
|
||||
responseFormat:
|
||||
$checkedConvert('responseFormat', (v) => v as String?),
|
||||
additionalParameters: $checkedConvert(
|
||||
'additionalParameters', (v) => v as Map<String, dynamic>?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LLMRequestToJson(LLMRequest instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('messages', instance.messages?.map((e) => e.toJson()).toList());
|
||||
writeNotNull('temperature', instance.temperature);
|
||||
writeNotNull('topP', instance.topP);
|
||||
writeNotNull('topK', instance.topK);
|
||||
writeNotNull('maxTokens', instance.maxTokens);
|
||||
writeNotNull('seed', instance.seed);
|
||||
writeNotNull('tools', instance.tools?.map((e) => e.toJson()).toList());
|
||||
writeNotNull('toolChoice', instance.toolChoice);
|
||||
writeNotNull('responseFormat', instance.responseFormat);
|
||||
writeNotNull('additionalParameters', instance.additionalParameters);
|
||||
return val;
|
||||
}
|
||||
|
||||
LLMResponse _$LLMResponseFromJson(Map<String, dynamic> json) => $checkedCreate(
|
||||
'LLMResponse',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = LLMResponse(
|
||||
id: $checkedConvert('id', (v) => v as String?),
|
||||
content: $checkedConvert('content', (v) => v as String?),
|
||||
tokenUsage: $checkedConvert(
|
||||
'tokenUsage',
|
||||
(v) => v == null
|
||||
? null
|
||||
: LLMTokenUsage.fromJson(v as Map<String, dynamic>)),
|
||||
finishReason: $checkedConvert('finishReason', (v) => v as String?),
|
||||
toolCallResults: $checkedConvert(
|
||||
'toolCallResults',
|
||||
(v) => (v as List<dynamic>?)
|
||||
?.map((e) =>
|
||||
LLMToolCallResult.fromJson(e as Map<String, dynamic>))
|
||||
.toList()),
|
||||
metadata:
|
||||
$checkedConvert('metadata', (v) => v as Map<String, dynamic>?),
|
||||
streamChunks: $checkedConvert('streamChunks',
|
||||
(v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LLMResponseToJson(LLMResponse instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('id', instance.id);
|
||||
writeNotNull('content', instance.content);
|
||||
writeNotNull('tokenUsage', instance.tokenUsage?.toJson());
|
||||
writeNotNull('finishReason', instance.finishReason);
|
||||
writeNotNull('toolCallResults',
|
||||
instance.toolCallResults?.map((e) => e.toJson()).toList());
|
||||
writeNotNull('metadata', instance.metadata);
|
||||
writeNotNull('streamChunks', instance.streamChunks);
|
||||
return val;
|
||||
}
|
||||
|
||||
LLMMessage _$LLMMessageFromJson(Map<String, dynamic> json) => $checkedCreate(
|
||||
'LLMMessage',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = LLMMessage(
|
||||
role: $checkedConvert('role', (v) => v as String),
|
||||
content: $checkedConvert('content', (v) => v as String?),
|
||||
name: $checkedConvert('name', (v) => v as String?),
|
||||
metadata:
|
||||
$checkedConvert('metadata', (v) => v as Map<String, dynamic>?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LLMMessageToJson(LLMMessage instance) {
|
||||
final val = <String, dynamic>{
|
||||
'role': instance.role,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('content', instance.content);
|
||||
writeNotNull('name', instance.name);
|
||||
writeNotNull('metadata', instance.metadata);
|
||||
return val;
|
||||
}
|
||||
|
||||
LLMTool _$LLMToolFromJson(Map<String, dynamic> json) => $checkedCreate(
|
||||
'LLMTool',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = LLMTool(
|
||||
name: $checkedConvert('name', (v) => v as String),
|
||||
description: $checkedConvert('description', (v) => v as String?),
|
||||
parameters:
|
||||
$checkedConvert('parameters', (v) => v as Map<String, dynamic>?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LLMToolToJson(LLMTool instance) {
|
||||
final val = <String, dynamic>{
|
||||
'name': instance.name,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('description', instance.description);
|
||||
writeNotNull('parameters', instance.parameters);
|
||||
return val;
|
||||
}
|
||||
|
||||
LLMToolCall _$LLMToolCallFromJson(Map<String, dynamic> json) => $checkedCreate(
|
||||
'LLMToolCall',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = LLMToolCall(
|
||||
id: $checkedConvert('id', (v) => v as String),
|
||||
name: $checkedConvert('name', (v) => v as String),
|
||||
arguments:
|
||||
$checkedConvert('arguments', (v) => v as Map<String, dynamic>?),
|
||||
timestamp: $checkedConvert('timestamp',
|
||||
(v) => v == null ? null : DateTime.parse(v as String)),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LLMToolCallToJson(LLMToolCall instance) {
|
||||
final val = <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('arguments', instance.arguments);
|
||||
writeNotNull('timestamp', instance.timestamp?.toIso8601String());
|
||||
return val;
|
||||
}
|
||||
|
||||
LLMToolCallResult _$LLMToolCallResultFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'LLMToolCallResult',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = LLMToolCallResult(
|
||||
toolCallId: $checkedConvert('toolCallId', (v) => v as String),
|
||||
result: $checkedConvert('result', (v) => v as String?),
|
||||
error: $checkedConvert(
|
||||
'error',
|
||||
(v) => v == null
|
||||
? null
|
||||
: LLMError.fromJson(v as Map<String, dynamic>)),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LLMToolCallResultToJson(LLMToolCallResult instance) {
|
||||
final val = <String, dynamic>{
|
||||
'toolCallId': instance.toolCallId,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('result', instance.result);
|
||||
writeNotNull('error', instance.error?.toJson());
|
||||
return val;
|
||||
}
|
||||
|
||||
LLMTokenUsage _$LLMTokenUsageFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'LLMTokenUsage',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = LLMTokenUsage(
|
||||
promptTokens:
|
||||
$checkedConvert('promptTokens', (v) => (v as num?)?.toInt()),
|
||||
completionTokens:
|
||||
$checkedConvert('completionTokens', (v) => (v as num?)?.toInt()),
|
||||
totalTokens:
|
||||
$checkedConvert('totalTokens', (v) => (v as num?)?.toInt()),
|
||||
inputTokens:
|
||||
$checkedConvert('inputTokens', (v) => (v as num?)?.toInt()),
|
||||
outputTokens:
|
||||
$checkedConvert('outputTokens', (v) => (v as num?)?.toInt()),
|
||||
reasoningTokens:
|
||||
$checkedConvert('reasoningTokens', (v) => (v as num?)?.toInt()),
|
||||
cachedTokens:
|
||||
$checkedConvert('cachedTokens', (v) => (v as num?)?.toInt()),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LLMTokenUsageToJson(LLMTokenUsage instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('promptTokens', instance.promptTokens);
|
||||
writeNotNull('completionTokens', instance.completionTokens);
|
||||
writeNotNull('totalTokens', instance.totalTokens);
|
||||
writeNotNull('inputTokens', instance.inputTokens);
|
||||
writeNotNull('outputTokens', instance.outputTokens);
|
||||
writeNotNull('reasoningTokens', instance.reasoningTokens);
|
||||
writeNotNull('cachedTokens', instance.cachedTokens);
|
||||
return val;
|
||||
}
|
||||
|
||||
LLMPerformanceMetrics _$LLMPerformanceMetricsFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'LLMPerformanceMetrics',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = LLMPerformanceMetrics(
|
||||
requestLatencyMs:
|
||||
$checkedConvert('requestLatencyMs', (v) => (v as num?)?.toInt()),
|
||||
firstTokenLatencyMs: $checkedConvert(
|
||||
'firstTokenLatencyMs', (v) => (v as num?)?.toInt()),
|
||||
totalDurationMs:
|
||||
$checkedConvert('totalDurationMs', (v) => (v as num?)?.toInt()),
|
||||
tokensPerSecond: $checkedConvert(
|
||||
'tokensPerSecond', (v) => (v as num?)?.toDouble()),
|
||||
charactersPerSecond: $checkedConvert(
|
||||
'charactersPerSecond', (v) => (v as num?)?.toDouble()),
|
||||
queueTimeMs:
|
||||
$checkedConvert('queueTimeMs', (v) => (v as num?)?.toInt()),
|
||||
processingTimeMs:
|
||||
$checkedConvert('processingTimeMs', (v) => (v as num?)?.toInt()),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LLMPerformanceMetricsToJson(
|
||||
LLMPerformanceMetrics instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('requestLatencyMs', instance.requestLatencyMs);
|
||||
writeNotNull('firstTokenLatencyMs', instance.firstTokenLatencyMs);
|
||||
writeNotNull('totalDurationMs', instance.totalDurationMs);
|
||||
writeNotNull('tokensPerSecond', instance.tokensPerSecond);
|
||||
writeNotNull('charactersPerSecond', instance.charactersPerSecond);
|
||||
writeNotNull('queueTimeMs', instance.queueTimeMs);
|
||||
writeNotNull('processingTimeMs', instance.processingTimeMs);
|
||||
return val;
|
||||
}
|
||||
|
||||
LLMError _$LLMErrorFromJson(Map<String, dynamic> json) => $checkedCreate(
|
||||
'LLMError',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = LLMError(
|
||||
type: $checkedConvert('type', (v) => v as String?),
|
||||
message: $checkedConvert('message', (v) => v as String?),
|
||||
code: $checkedConvert('code', (v) => v as String?),
|
||||
stackTrace: $checkedConvert('stackTrace', (v) => v as String?),
|
||||
details:
|
||||
$checkedConvert('details', (v) => v as Map<String, dynamic>?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LLMErrorToJson(LLMError instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('type', instance.type);
|
||||
writeNotNull('message', instance.message);
|
||||
writeNotNull('code', instance.code);
|
||||
writeNotNull('stackTrace', instance.stackTrace);
|
||||
writeNotNull('details', instance.details);
|
||||
return val;
|
||||
}
|
||||
|
||||
LLMStatistics _$LLMStatisticsFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'LLMStatistics',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = LLMStatistics(
|
||||
totalCalls: $checkedConvert('totalCalls', (v) => (v as num).toInt()),
|
||||
successfulCalls:
|
||||
$checkedConvert('successfulCalls', (v) => (v as num).toInt()),
|
||||
failedCalls:
|
||||
$checkedConvert('failedCalls', (v) => (v as num).toInt()),
|
||||
successRate:
|
||||
$checkedConvert('successRate', (v) => (v as num).toDouble()),
|
||||
averageLatency:
|
||||
$checkedConvert('averageLatency', (v) => (v as num).toDouble()),
|
||||
totalTokens:
|
||||
$checkedConvert('totalTokens', (v) => (v as num).toInt()),
|
||||
startTime: $checkedConvert('startTime',
|
||||
(v) => v == null ? null : DateTime.parse(v as String)),
|
||||
endTime: $checkedConvert(
|
||||
'endTime', (v) => v == null ? null : DateTime.parse(v as String)),
|
||||
details:
|
||||
$checkedConvert('details', (v) => v as Map<String, dynamic>?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LLMStatisticsToJson(LLMStatistics instance) {
|
||||
final val = <String, dynamic>{
|
||||
'totalCalls': instance.totalCalls,
|
||||
'successfulCalls': instance.successfulCalls,
|
||||
'failedCalls': instance.failedCalls,
|
||||
'successRate': instance.successRate,
|
||||
'averageLatency': instance.averageLatency,
|
||||
'totalTokens': instance.totalTokens,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('startTime', instance.startTime?.toIso8601String());
|
||||
writeNotNull('endTime', instance.endTime?.toIso8601String());
|
||||
writeNotNull('details', instance.details);
|
||||
return val;
|
||||
}
|
||||
|
||||
ProviderStatistics _$ProviderStatisticsFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'ProviderStatistics',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = ProviderStatistics(
|
||||
provider: $checkedConvert('provider', (v) => v as String),
|
||||
statistics: $checkedConvert('statistics',
|
||||
(v) => LLMStatistics.fromJson(v as Map<String, dynamic>)),
|
||||
models: $checkedConvert(
|
||||
'models',
|
||||
(v) => (v as List<dynamic>)
|
||||
.map((e) =>
|
||||
ModelStatistics.fromJson(e as Map<String, dynamic>))
|
||||
.toList()),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ProviderStatisticsToJson(ProviderStatistics instance) =>
|
||||
<String, dynamic>{
|
||||
'provider': instance.provider,
|
||||
'statistics': instance.statistics.toJson(),
|
||||
'models': instance.models.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
ModelStatistics _$ModelStatisticsFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'ModelStatistics',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = ModelStatistics(
|
||||
modelName: $checkedConvert('modelName', (v) => v as String),
|
||||
provider: $checkedConvert('provider', (v) => v as String),
|
||||
statistics: $checkedConvert('statistics',
|
||||
(v) => LLMStatistics.fromJson(v as Map<String, dynamic>)),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ModelStatisticsToJson(ModelStatistics instance) =>
|
||||
<String, dynamic>{
|
||||
'modelName': instance.modelName,
|
||||
'provider': instance.provider,
|
||||
'statistics': instance.statistics.toJson(),
|
||||
};
|
||||
|
||||
UserStatistics _$UserStatisticsFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'UserStatistics',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = UserStatistics(
|
||||
userId: $checkedConvert('userId', (v) => v as String),
|
||||
username: $checkedConvert('username', (v) => v as String?),
|
||||
statistics: $checkedConvert('statistics',
|
||||
(v) => LLMStatistics.fromJson(v as Map<String, dynamic>)),
|
||||
topModels: $checkedConvert('topModels',
|
||||
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
|
||||
topProviders: $checkedConvert('topProviders',
|
||||
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UserStatisticsToJson(UserStatistics instance) {
|
||||
final val = <String, dynamic>{
|
||||
'userId': instance.userId,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('username', instance.username);
|
||||
val['statistics'] = instance.statistics.toJson();
|
||||
val['topModels'] = instance.topModels;
|
||||
val['topProviders'] = instance.topProviders;
|
||||
return val;
|
||||
}
|
||||
|
||||
ErrorStatistics _$ErrorStatisticsFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'ErrorStatistics',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = ErrorStatistics(
|
||||
errorType: $checkedConvert('errorType', (v) => v as String),
|
||||
count: $checkedConvert('count', (v) => (v as num).toInt()),
|
||||
percentage:
|
||||
$checkedConvert('percentage', (v) => (v as num).toDouble()),
|
||||
topErrorMessages: $checkedConvert('topErrorMessages',
|
||||
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
|
||||
affectedModels: $checkedConvert('affectedModels',
|
||||
(v) => (v as List<dynamic>).map((e) => e as String).toList()),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ErrorStatisticsToJson(ErrorStatistics instance) =>
|
||||
<String, dynamic>{
|
||||
'errorType': instance.errorType,
|
||||
'count': instance.count,
|
||||
'percentage': instance.percentage,
|
||||
'topErrorMessages': instance.topErrorMessages,
|
||||
'affectedModels': instance.affectedModels,
|
||||
};
|
||||
|
||||
PerformanceStatistics _$PerformanceStatisticsFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'PerformanceStatistics',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = PerformanceStatistics(
|
||||
averageLatency:
|
||||
$checkedConvert('averageLatency', (v) => (v as num).toDouble()),
|
||||
medianLatency:
|
||||
$checkedConvert('medianLatency', (v) => (v as num).toDouble()),
|
||||
p95Latency:
|
||||
$checkedConvert('p95Latency', (v) => (v as num).toDouble()),
|
||||
p99Latency:
|
||||
$checkedConvert('p99Latency', (v) => (v as num).toDouble()),
|
||||
averageThroughput: $checkedConvert(
|
||||
'averageThroughput', (v) => (v as num).toDouble()),
|
||||
latencyTrends: $checkedConvert(
|
||||
'latencyTrends',
|
||||
(v) => (v as List<dynamic>)
|
||||
.map((e) =>
|
||||
TimeBasedMetric.fromJson(e as Map<String, dynamic>))
|
||||
.toList()),
|
||||
throughputTrends: $checkedConvert(
|
||||
'throughputTrends',
|
||||
(v) => (v as List<dynamic>)
|
||||
.map((e) =>
|
||||
TimeBasedMetric.fromJson(e as Map<String, dynamic>))
|
||||
.toList()),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PerformanceStatisticsToJson(
|
||||
PerformanceStatistics instance) =>
|
||||
<String, dynamic>{
|
||||
'averageLatency': instance.averageLatency,
|
||||
'medianLatency': instance.medianLatency,
|
||||
'p95Latency': instance.p95Latency,
|
||||
'p99Latency': instance.p99Latency,
|
||||
'averageThroughput': instance.averageThroughput,
|
||||
'latencyTrends': instance.latencyTrends.map((e) => e.toJson()).toList(),
|
||||
'throughputTrends':
|
||||
instance.throughputTrends.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
TimeBasedMetric _$TimeBasedMetricFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'TimeBasedMetric',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = TimeBasedMetric(
|
||||
timestamp:
|
||||
$checkedConvert('timestamp', (v) => DateTime.parse(v as String)),
|
||||
value: $checkedConvert('value', (v) => (v as num).toDouble()),
|
||||
label: $checkedConvert('label', (v) => v as String?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TimeBasedMetricToJson(TimeBasedMetric instance) {
|
||||
final val = <String, dynamic>{
|
||||
'timestamp': instance.timestamp.toIso8601String(),
|
||||
'value': instance.value,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('label', instance.label);
|
||||
return val;
|
||||
}
|
||||
|
||||
SystemHealthStatus _$SystemHealthStatusFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'SystemHealthStatus',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = SystemHealthStatus(
|
||||
status: $checkedConvert(
|
||||
'status',
|
||||
(v) =>
|
||||
$enumDecodeNullable(_$HealthStatusEnumMap, v) ??
|
||||
HealthStatus.healthy),
|
||||
components: $checkedConvert(
|
||||
'components',
|
||||
(v) => (v as Map<String, dynamic>).map(
|
||||
(k, e) => MapEntry(
|
||||
k, ComponentHealth.fromJson(e as Map<String, dynamic>)),
|
||||
)),
|
||||
message: $checkedConvert('message', (v) => v as String?),
|
||||
lastChecked: $checkedConvert('lastChecked',
|
||||
(v) => v == null ? null : DateTime.parse(v as String)),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SystemHealthStatusToJson(SystemHealthStatus instance) {
|
||||
final val = <String, dynamic>{
|
||||
'status': _$HealthStatusEnumMap[instance.status]!,
|
||||
'components': instance.components.map((k, e) => MapEntry(k, e.toJson())),
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('message', instance.message);
|
||||
writeNotNull('lastChecked', instance.lastChecked?.toIso8601String());
|
||||
return val;
|
||||
}
|
||||
|
||||
const _$HealthStatusEnumMap = {
|
||||
HealthStatus.healthy: 'healthy',
|
||||
HealthStatus.degraded: 'degraded',
|
||||
HealthStatus.unhealthy: 'unhealthy',
|
||||
HealthStatus.unknown: 'unknown',
|
||||
};
|
||||
|
||||
ComponentHealth _$ComponentHealthFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'ComponentHealth',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = ComponentHealth(
|
||||
status: $checkedConvert(
|
||||
'status',
|
||||
(v) =>
|
||||
$enumDecodeNullable(_$HealthStatusEnumMap, v) ??
|
||||
HealthStatus.healthy),
|
||||
message: $checkedConvert('message', (v) => v as String?),
|
||||
metrics:
|
||||
$checkedConvert('metrics', (v) => v as Map<String, dynamic>?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ComponentHealthToJson(ComponentHealth instance) {
|
||||
final val = <String, dynamic>{
|
||||
'status': _$HealthStatusEnumMap[instance.status]!,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('message', instance.message);
|
||||
writeNotNull('metrics', instance.metrics);
|
||||
return val;
|
||||
}
|
||||
|
||||
LLMTraceSearchCriteria _$LLMTraceSearchCriteriaFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'LLMTraceSearchCriteria',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = LLMTraceSearchCriteria(
|
||||
userId: $checkedConvert('userId', (v) => v as String?),
|
||||
provider: $checkedConvert('provider', (v) => v as String?),
|
||||
model: $checkedConvert('model', (v) => v as String?),
|
||||
sessionId: $checkedConvert('sessionId', (v) => v as String?),
|
||||
hasError: $checkedConvert('hasError', (v) => v as bool?),
|
||||
status: $checkedConvert(
|
||||
'status', (v) => $enumDecodeNullable(_$LLMTraceStatusEnumMap, v)),
|
||||
startTime: $checkedConvert('startTime',
|
||||
(v) => v == null ? null : DateTime.parse(v as String)),
|
||||
endTime: $checkedConvert(
|
||||
'endTime', (v) => v == null ? null : DateTime.parse(v as String)),
|
||||
page: $checkedConvert('page', (v) => (v as num?)?.toInt() ?? 0),
|
||||
size: $checkedConvert('size', (v) => (v as num?)?.toInt() ?? 20),
|
||||
sortBy: $checkedConvert('sortBy', (v) => v as String? ?? 'timestamp'),
|
||||
sortDir: $checkedConvert('sortDir', (v) => v as String? ?? 'desc'),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$LLMTraceSearchCriteriaToJson(
|
||||
LLMTraceSearchCriteria instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('userId', instance.userId);
|
||||
writeNotNull('provider', instance.provider);
|
||||
writeNotNull('model', instance.model);
|
||||
writeNotNull('sessionId', instance.sessionId);
|
||||
writeNotNull('hasError', instance.hasError);
|
||||
writeNotNull('status', _$LLMTraceStatusEnumMap[instance.status]);
|
||||
writeNotNull('startTime', instance.startTime?.toIso8601String());
|
||||
writeNotNull('endTime', instance.endTime?.toIso8601String());
|
||||
val['page'] = instance.page;
|
||||
val['size'] = instance.size;
|
||||
val['sortBy'] = instance.sortBy;
|
||||
val['sortDir'] = instance.sortDir;
|
||||
return val;
|
||||
}
|
||||
|
||||
ApiResponse<T> _$ApiResponseFromJson<T>(
|
||||
Map<String, dynamic> json,
|
||||
T Function(Object? json) fromJsonT,
|
||||
) =>
|
||||
$checkedCreate(
|
||||
'ApiResponse',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = ApiResponse<T>(
|
||||
success: $checkedConvert('success', (v) => v as bool),
|
||||
message: $checkedConvert('message', (v) => v as String?),
|
||||
data: $checkedConvert(
|
||||
'data', (v) => _$nullableGenericFromJson(v, fromJsonT)),
|
||||
error: $checkedConvert('error', (v) => v as String?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ApiResponseToJson<T>(
|
||||
ApiResponse<T> instance,
|
||||
Object? Function(T value) toJsonT,
|
||||
) {
|
||||
final val = <String, dynamic>{
|
||||
'success': instance.success,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('message', instance.message);
|
||||
writeNotNull('data', _$nullableGenericToJson(instance.data, toJsonT));
|
||||
writeNotNull('error', instance.error);
|
||||
return val;
|
||||
}
|
||||
|
||||
T? _$nullableGenericFromJson<T>(
|
||||
Object? input,
|
||||
T Function(Object? json) fromJson,
|
||||
) =>
|
||||
input == null ? null : fromJson(input);
|
||||
|
||||
Object? _$nullableGenericToJson<T>(
|
||||
T? input,
|
||||
Object? Function(T value) toJson,
|
||||
) =>
|
||||
input == null ? null : toJson(input);
|
||||
|
||||
PagedResponse<T> _$PagedResponseFromJson<T>(
|
||||
Map<String, dynamic> json,
|
||||
T Function(Object? json) fromJsonT,
|
||||
) =>
|
||||
$checkedCreate(
|
||||
'PagedResponse',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = PagedResponse<T>(
|
||||
content: $checkedConvert(
|
||||
'content', (v) => (v as List<dynamic>).map(fromJsonT).toList()),
|
||||
page: $checkedConvert('page', (v) => (v as num).toInt()),
|
||||
size: $checkedConvert('size', (v) => (v as num).toInt()),
|
||||
totalElements:
|
||||
$checkedConvert('totalElements', (v) => (v as num).toInt()),
|
||||
totalPages: $checkedConvert('totalPages', (v) => (v as num).toInt()),
|
||||
first: $checkedConvert('first', (v) => v as bool? ?? false),
|
||||
last: $checkedConvert('last', (v) => v as bool? ?? false),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PagedResponseToJson<T>(
|
||||
PagedResponse<T> instance,
|
||||
Object? Function(T value) toJsonT,
|
||||
) =>
|
||||
<String, dynamic>{
|
||||
'content': instance.content.map(toJsonT).toList(),
|
||||
'page': instance.page,
|
||||
'size': instance.size,
|
||||
'totalElements': instance.totalElements,
|
||||
'totalPages': instance.totalPages,
|
||||
'first': instance.first,
|
||||
'last': instance.last,
|
||||
};
|
||||
|
||||
CursorPageResponse<T> _$CursorPageResponseFromJson<T>(
|
||||
Map<String, dynamic> json,
|
||||
T Function(Object? json) fromJsonT,
|
||||
) =>
|
||||
$checkedCreate(
|
||||
'CursorPageResponse',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = CursorPageResponse<T>(
|
||||
items: $checkedConvert(
|
||||
'items', (v) => (v as List<dynamic>).map(fromJsonT).toList()),
|
||||
nextCursor: $checkedConvert('nextCursor', (v) => v as String?),
|
||||
hasMore: $checkedConvert('hasMore', (v) => v as bool? ?? false),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CursorPageResponseToJson<T>(
|
||||
CursorPageResponse<T> instance,
|
||||
Object? Function(T value) toJsonT,
|
||||
) {
|
||||
final val = <String, dynamic>{
|
||||
'items': instance.items.map(toJsonT).toList(),
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('nextCursor', instance.nextCursor);
|
||||
val['hasMore'] = instance.hasMore;
|
||||
return val;
|
||||
}
|
||||
429
AINoval/lib/models/admin/subscription_models.dart
Normal file
429
AINoval/lib/models/admin/subscription_models.dart
Normal file
@@ -0,0 +1,429 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import '../../utils/date_time_parser.dart';
|
||||
|
||||
part 'subscription_models.g.dart';
|
||||
|
||||
/// 订阅计划模型
|
||||
@JsonSerializable()
|
||||
class SubscriptionPlan extends Equatable {
|
||||
final String? id;
|
||||
final String planName;
|
||||
final String? description;
|
||||
final double price;
|
||||
final String currency;
|
||||
final BillingCycle billingCycle;
|
||||
final String? roleId;
|
||||
final int? creditsGranted;
|
||||
final bool active;
|
||||
final bool recommended;
|
||||
final int priority;
|
||||
final Map<String, dynamic>? features;
|
||||
final int trialDays;
|
||||
final int maxUsers;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
const SubscriptionPlan({
|
||||
this.id,
|
||||
required this.planName,
|
||||
this.description,
|
||||
required this.price,
|
||||
required this.currency,
|
||||
required this.billingCycle,
|
||||
this.roleId,
|
||||
this.creditsGranted,
|
||||
this.active = true,
|
||||
this.recommended = false,
|
||||
this.priority = 0,
|
||||
this.features,
|
||||
this.trialDays = 0,
|
||||
this.maxUsers = -1,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
factory SubscriptionPlan.fromJson(Map<String, dynamic> json) {
|
||||
// 兼容后端字段可能为空或类型不一致(如 BigDecimal 序列化为字符串)
|
||||
final dynamic priceRaw = json['price'];
|
||||
final double parsedPrice = priceRaw is num
|
||||
? priceRaw.toDouble()
|
||||
: (priceRaw is String ? double.tryParse(priceRaw) ?? 0.0 : 0.0);
|
||||
|
||||
final dynamic priorityRaw = json['priority'];
|
||||
final int parsedPriority = priorityRaw is num
|
||||
? priorityRaw.toInt()
|
||||
: (priorityRaw is String ? int.tryParse(priorityRaw) ?? 0 : 0);
|
||||
|
||||
final dynamic creditsRaw = json['creditsGranted'];
|
||||
final int? parsedCredits = creditsRaw == null
|
||||
? null
|
||||
: (creditsRaw is num
|
||||
? creditsRaw.toInt()
|
||||
: (creditsRaw is String ? int.tryParse(creditsRaw) : null));
|
||||
|
||||
final dynamic activeRaw = json['active'];
|
||||
final bool parsedActive = activeRaw is bool
|
||||
? activeRaw
|
||||
: (activeRaw is String ? activeRaw.toLowerCase() == 'true' : true);
|
||||
|
||||
final dynamic recommendedRaw = json['recommended'];
|
||||
final bool parsedRecommended = recommendedRaw is bool
|
||||
? recommendedRaw
|
||||
: (recommendedRaw is String ? recommendedRaw.toLowerCase() == 'true' : false);
|
||||
|
||||
final featuresRaw = json['features'];
|
||||
final Map<String, dynamic>? parsedFeatures =
|
||||
featuresRaw is Map<String, dynamic> ? featuresRaw : null;
|
||||
|
||||
return SubscriptionPlan(
|
||||
id: json['id'] as String?,
|
||||
planName: (json['planName'] as String?) ?? '未命名套餐',
|
||||
description: json['description'] as String?,
|
||||
price: parsedPrice,
|
||||
currency: (json['currency'] as String?) ?? 'CNY',
|
||||
billingCycle: _parseBillingCycle(json['billingCycle']),
|
||||
roleId: json['roleId'] as String?,
|
||||
creditsGranted: parsedCredits,
|
||||
active: parsedActive,
|
||||
recommended: parsedRecommended,
|
||||
priority: parsedPriority,
|
||||
features: parsedFeatures,
|
||||
trialDays: ((json['trialDays'] is String)
|
||||
? int.tryParse(json['trialDays'])
|
||||
: (json['trialDays'] as num?))
|
||||
?.toInt() ?? 0,
|
||||
maxUsers: ((json['maxUsers'] is String)
|
||||
? int.tryParse(json['maxUsers'])
|
||||
: (json['maxUsers'] as num?))
|
||||
?.toInt() ?? -1,
|
||||
createdAt: json['createdAt'] != null ? parseBackendDateTime(json['createdAt']) : null,
|
||||
updatedAt: json['updatedAt'] != null ? parseBackendDateTime(json['updatedAt']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$SubscriptionPlanToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
planName,
|
||||
description,
|
||||
price,
|
||||
currency,
|
||||
billingCycle,
|
||||
roleId,
|
||||
creditsGranted,
|
||||
active,
|
||||
recommended,
|
||||
priority,
|
||||
features,
|
||||
trialDays,
|
||||
maxUsers,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
];
|
||||
|
||||
/// 获取月度等价价格
|
||||
double get monthlyEquivalentPrice {
|
||||
switch (billingCycle) {
|
||||
case BillingCycle.monthly:
|
||||
return price;
|
||||
case BillingCycle.quarterly:
|
||||
return price / 3;
|
||||
case BillingCycle.yearly:
|
||||
return price / 12;
|
||||
case BillingCycle.lifetime:
|
||||
return price / 120; // 假设10年使用期
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取计费周期显示文本
|
||||
String get billingCycleText {
|
||||
switch (billingCycle) {
|
||||
case BillingCycle.monthly:
|
||||
return '月付';
|
||||
case BillingCycle.quarterly:
|
||||
return '季付';
|
||||
case BillingCycle.yearly:
|
||||
return '年付';
|
||||
case BillingCycle.lifetime:
|
||||
return '终身';
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取格式化价格
|
||||
String get formattedPrice {
|
||||
return '$currency ${price.toStringAsFixed(2)}';
|
||||
}
|
||||
|
||||
/// 解析BillingCycle枚举
|
||||
static BillingCycle _parseBillingCycle(dynamic value) {
|
||||
if (value == null) return BillingCycle.monthly;
|
||||
|
||||
final stringValue = value.toString().toUpperCase();
|
||||
switch (stringValue) {
|
||||
case 'MONTHLY':
|
||||
return BillingCycle.monthly;
|
||||
case 'QUARTERLY':
|
||||
return BillingCycle.quarterly;
|
||||
case 'YEARLY':
|
||||
return BillingCycle.yearly;
|
||||
case 'LIFETIME':
|
||||
return BillingCycle.lifetime;
|
||||
default:
|
||||
return BillingCycle.monthly;
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建副本
|
||||
SubscriptionPlan copyWith({
|
||||
String? id,
|
||||
String? planName,
|
||||
String? description,
|
||||
double? price,
|
||||
String? currency,
|
||||
BillingCycle? billingCycle,
|
||||
String? roleId,
|
||||
int? creditsGranted,
|
||||
bool? active,
|
||||
bool? recommended,
|
||||
int? priority,
|
||||
Map<String, dynamic>? features,
|
||||
int? trialDays,
|
||||
int? maxUsers,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return SubscriptionPlan(
|
||||
id: id ?? this.id,
|
||||
planName: planName ?? this.planName,
|
||||
description: description ?? this.description,
|
||||
price: price ?? this.price,
|
||||
currency: currency ?? this.currency,
|
||||
billingCycle: billingCycle ?? this.billingCycle,
|
||||
roleId: roleId ?? this.roleId,
|
||||
creditsGranted: creditsGranted ?? this.creditsGranted,
|
||||
active: active ?? this.active,
|
||||
recommended: recommended ?? this.recommended,
|
||||
priority: priority ?? this.priority,
|
||||
features: features ?? this.features,
|
||||
trialDays: trialDays ?? this.trialDays,
|
||||
maxUsers: maxUsers ?? this.maxUsers,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 计费周期枚举
|
||||
enum BillingCycle {
|
||||
@JsonValue('MONTHLY')
|
||||
monthly,
|
||||
@JsonValue('QUARTERLY')
|
||||
quarterly,
|
||||
@JsonValue('YEARLY')
|
||||
yearly,
|
||||
@JsonValue('LIFETIME')
|
||||
lifetime,
|
||||
}
|
||||
|
||||
/// 用户订阅模型
|
||||
@JsonSerializable()
|
||||
class UserSubscription extends Equatable {
|
||||
final String? id;
|
||||
final String userId;
|
||||
final String planId;
|
||||
final DateTime? startDate;
|
||||
final DateTime? endDate;
|
||||
final SubscriptionStatus status;
|
||||
final bool autoRenewal;
|
||||
final String? paymentMethod;
|
||||
final String? transactionId;
|
||||
final int creditsUsed;
|
||||
final int totalCredits;
|
||||
final DateTime? canceledAt;
|
||||
final String? cancelReason;
|
||||
final DateTime? trialEndDate;
|
||||
final bool isTrial;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
const UserSubscription({
|
||||
this.id,
|
||||
required this.userId,
|
||||
required this.planId,
|
||||
this.startDate,
|
||||
this.endDate,
|
||||
required this.status,
|
||||
this.autoRenewal = false,
|
||||
this.paymentMethod,
|
||||
this.transactionId,
|
||||
this.creditsUsed = 0,
|
||||
this.totalCredits = 0,
|
||||
this.canceledAt,
|
||||
this.cancelReason,
|
||||
this.trialEndDate,
|
||||
this.isTrial = false,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
factory UserSubscription.fromJson(Map<String, dynamic> json) {
|
||||
return UserSubscription(
|
||||
id: json['id'] as String?,
|
||||
userId: json['userId'] as String,
|
||||
planId: json['planId'] as String,
|
||||
startDate: json['startDate'] != null ? parseBackendDateTime(json['startDate']) : null,
|
||||
endDate: json['endDate'] != null ? parseBackendDateTime(json['endDate']) : null,
|
||||
status: _parseSubscriptionStatus(json['status']),
|
||||
autoRenewal: json['autoRenewal'] as bool? ?? false,
|
||||
paymentMethod: json['paymentMethod'] as String?,
|
||||
transactionId: json['transactionId'] as String?,
|
||||
creditsUsed: (json['creditsUsed'] as num?)?.toInt() ?? 0,
|
||||
totalCredits: (json['totalCredits'] as num?)?.toInt() ?? 0,
|
||||
canceledAt: json['canceledAt'] != null ? parseBackendDateTime(json['canceledAt']) : null,
|
||||
cancelReason: json['cancelReason'] as String?,
|
||||
trialEndDate: json['trialEndDate'] != null ? parseBackendDateTime(json['trialEndDate']) : null,
|
||||
isTrial: json['isTrial'] as bool? ?? false,
|
||||
createdAt: json['createdAt'] != null ? parseBackendDateTime(json['createdAt']) : null,
|
||||
updatedAt: json['updatedAt'] != null ? parseBackendDateTime(json['updatedAt']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => _$UserSubscriptionToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
userId,
|
||||
planId,
|
||||
startDate,
|
||||
endDate,
|
||||
status,
|
||||
autoRenewal,
|
||||
paymentMethod,
|
||||
transactionId,
|
||||
creditsUsed,
|
||||
totalCredits,
|
||||
canceledAt,
|
||||
cancelReason,
|
||||
trialEndDate,
|
||||
isTrial,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
];
|
||||
|
||||
/// 获取剩余积分
|
||||
int get remainingCredits => (totalCredits - creditsUsed).clamp(0, totalCredits);
|
||||
|
||||
/// 检查订阅是否有效
|
||||
bool get isValid {
|
||||
final now = DateTime.now();
|
||||
return (status == SubscriptionStatus.active || status == SubscriptionStatus.trial) &&
|
||||
(endDate == null || endDate!.isAfter(now));
|
||||
}
|
||||
|
||||
/// 检查是否即将过期(7天内)
|
||||
bool get isExpiringSoon {
|
||||
if (endDate == null) return false;
|
||||
final now = DateTime.now();
|
||||
final sevenDaysLater = now.add(const Duration(days: 7));
|
||||
return endDate!.isBefore(sevenDaysLater) && endDate!.isAfter(now);
|
||||
}
|
||||
|
||||
/// 解析SubscriptionStatus枚举
|
||||
static SubscriptionStatus _parseSubscriptionStatus(dynamic value) {
|
||||
if (value == null) return SubscriptionStatus.active;
|
||||
|
||||
final stringValue = value.toString().toUpperCase();
|
||||
switch (stringValue) {
|
||||
case 'ACTIVE':
|
||||
return SubscriptionStatus.active;
|
||||
case 'TRIAL':
|
||||
return SubscriptionStatus.trial;
|
||||
case 'CANCELED':
|
||||
return SubscriptionStatus.canceled;
|
||||
case 'EXPIRED':
|
||||
return SubscriptionStatus.expired;
|
||||
case 'SUSPENDED':
|
||||
return SubscriptionStatus.suspended;
|
||||
case 'REFUNDED':
|
||||
return SubscriptionStatus.refunded;
|
||||
default:
|
||||
return SubscriptionStatus.active;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取状态显示文本
|
||||
String get statusText {
|
||||
switch (status) {
|
||||
case SubscriptionStatus.active:
|
||||
return '活跃';
|
||||
case SubscriptionStatus.trial:
|
||||
return '试用期';
|
||||
case SubscriptionStatus.canceled:
|
||||
return '已取消';
|
||||
case SubscriptionStatus.expired:
|
||||
return '已过期';
|
||||
case SubscriptionStatus.suspended:
|
||||
return '暂停';
|
||||
case SubscriptionStatus.refunded:
|
||||
return '已退款';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 订阅状态枚举
|
||||
enum SubscriptionStatus {
|
||||
@JsonValue('ACTIVE')
|
||||
active,
|
||||
@JsonValue('TRIAL')
|
||||
trial,
|
||||
@JsonValue('CANCELED')
|
||||
canceled,
|
||||
@JsonValue('EXPIRED')
|
||||
expired,
|
||||
@JsonValue('SUSPENDED')
|
||||
suspended,
|
||||
@JsonValue('REFUNDED')
|
||||
refunded,
|
||||
}
|
||||
|
||||
/// 订阅统计信息
|
||||
@JsonSerializable()
|
||||
class SubscriptionStatistics extends Equatable {
|
||||
final int totalPlans;
|
||||
final int activePlans;
|
||||
final int totalSubscriptions;
|
||||
final int activeSubscriptions;
|
||||
final int trialSubscriptions;
|
||||
final double monthlyRevenue;
|
||||
final double yearlyRevenue;
|
||||
|
||||
const SubscriptionStatistics({
|
||||
required this.totalPlans,
|
||||
required this.activePlans,
|
||||
required this.totalSubscriptions,
|
||||
required this.activeSubscriptions,
|
||||
required this.trialSubscriptions,
|
||||
required this.monthlyRevenue,
|
||||
required this.yearlyRevenue,
|
||||
});
|
||||
|
||||
factory SubscriptionStatistics.fromJson(Map<String, dynamic> json) =>
|
||||
_$SubscriptionStatisticsFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SubscriptionStatisticsToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
totalPlans,
|
||||
activePlans,
|
||||
totalSubscriptions,
|
||||
activeSubscriptions,
|
||||
trialSubscriptions,
|
||||
monthlyRevenue,
|
||||
yearlyRevenue,
|
||||
];
|
||||
}
|
||||
191
AINoval/lib/models/admin/subscription_models.g.dart
Normal file
191
AINoval/lib/models/admin/subscription_models.g.dart
Normal file
@@ -0,0 +1,191 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'subscription_models.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SubscriptionPlan _$SubscriptionPlanFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'SubscriptionPlan',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = SubscriptionPlan(
|
||||
id: $checkedConvert('id', (v) => v as String?),
|
||||
planName: $checkedConvert('planName', (v) => v as String),
|
||||
description: $checkedConvert('description', (v) => v as String?),
|
||||
price: $checkedConvert('price', (v) => (v as num).toDouble()),
|
||||
currency: $checkedConvert('currency', (v) => v as String),
|
||||
billingCycle: $checkedConvert(
|
||||
'billingCycle', (v) => $enumDecode(_$BillingCycleEnumMap, v)),
|
||||
roleId: $checkedConvert('roleId', (v) => v as String?),
|
||||
creditsGranted:
|
||||
$checkedConvert('creditsGranted', (v) => (v as num?)?.toInt()),
|
||||
active: $checkedConvert('active', (v) => v as bool? ?? true),
|
||||
recommended:
|
||||
$checkedConvert('recommended', (v) => v as bool? ?? false),
|
||||
priority:
|
||||
$checkedConvert('priority', (v) => (v as num?)?.toInt() ?? 0),
|
||||
features:
|
||||
$checkedConvert('features', (v) => v as Map<String, dynamic>?),
|
||||
trialDays:
|
||||
$checkedConvert('trialDays', (v) => (v as num?)?.toInt() ?? 0),
|
||||
maxUsers:
|
||||
$checkedConvert('maxUsers', (v) => (v as num?)?.toInt() ?? -1),
|
||||
createdAt: $checkedConvert('createdAt',
|
||||
(v) => v == null ? null : DateTime.parse(v as String)),
|
||||
updatedAt: $checkedConvert('updatedAt',
|
||||
(v) => v == null ? null : DateTime.parse(v as String)),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SubscriptionPlanToJson(SubscriptionPlan instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('id', instance.id);
|
||||
val['planName'] = instance.planName;
|
||||
writeNotNull('description', instance.description);
|
||||
val['price'] = instance.price;
|
||||
val['currency'] = instance.currency;
|
||||
val['billingCycle'] = _$BillingCycleEnumMap[instance.billingCycle]!;
|
||||
writeNotNull('roleId', instance.roleId);
|
||||
writeNotNull('creditsGranted', instance.creditsGranted);
|
||||
val['active'] = instance.active;
|
||||
val['recommended'] = instance.recommended;
|
||||
val['priority'] = instance.priority;
|
||||
writeNotNull('features', instance.features);
|
||||
val['trialDays'] = instance.trialDays;
|
||||
val['maxUsers'] = instance.maxUsers;
|
||||
writeNotNull('createdAt', instance.createdAt?.toIso8601String());
|
||||
writeNotNull('updatedAt', instance.updatedAt?.toIso8601String());
|
||||
return val;
|
||||
}
|
||||
|
||||
const _$BillingCycleEnumMap = {
|
||||
BillingCycle.monthly: 'MONTHLY',
|
||||
BillingCycle.quarterly: 'QUARTERLY',
|
||||
BillingCycle.yearly: 'YEARLY',
|
||||
BillingCycle.lifetime: 'LIFETIME',
|
||||
};
|
||||
|
||||
UserSubscription _$UserSubscriptionFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'UserSubscription',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = UserSubscription(
|
||||
id: $checkedConvert('id', (v) => v as String?),
|
||||
userId: $checkedConvert('userId', (v) => v as String),
|
||||
planId: $checkedConvert('planId', (v) => v as String),
|
||||
startDate: $checkedConvert('startDate',
|
||||
(v) => v == null ? null : DateTime.parse(v as String)),
|
||||
endDate: $checkedConvert(
|
||||
'endDate', (v) => v == null ? null : DateTime.parse(v as String)),
|
||||
status: $checkedConvert(
|
||||
'status', (v) => $enumDecode(_$SubscriptionStatusEnumMap, v)),
|
||||
autoRenewal:
|
||||
$checkedConvert('autoRenewal', (v) => v as bool? ?? false),
|
||||
paymentMethod: $checkedConvert('paymentMethod', (v) => v as String?),
|
||||
transactionId: $checkedConvert('transactionId', (v) => v as String?),
|
||||
creditsUsed:
|
||||
$checkedConvert('creditsUsed', (v) => (v as num?)?.toInt() ?? 0),
|
||||
totalCredits:
|
||||
$checkedConvert('totalCredits', (v) => (v as num?)?.toInt() ?? 0),
|
||||
canceledAt: $checkedConvert('canceledAt',
|
||||
(v) => v == null ? null : DateTime.parse(v as String)),
|
||||
cancelReason: $checkedConvert('cancelReason', (v) => v as String?),
|
||||
trialEndDate: $checkedConvert('trialEndDate',
|
||||
(v) => v == null ? null : DateTime.parse(v as String)),
|
||||
isTrial: $checkedConvert('isTrial', (v) => v as bool? ?? false),
|
||||
createdAt: $checkedConvert('createdAt',
|
||||
(v) => v == null ? null : DateTime.parse(v as String)),
|
||||
updatedAt: $checkedConvert('updatedAt',
|
||||
(v) => v == null ? null : DateTime.parse(v as String)),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UserSubscriptionToJson(UserSubscription instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('id', instance.id);
|
||||
val['userId'] = instance.userId;
|
||||
val['planId'] = instance.planId;
|
||||
writeNotNull('startDate', instance.startDate?.toIso8601String());
|
||||
writeNotNull('endDate', instance.endDate?.toIso8601String());
|
||||
val['status'] = _$SubscriptionStatusEnumMap[instance.status]!;
|
||||
val['autoRenewal'] = instance.autoRenewal;
|
||||
writeNotNull('paymentMethod', instance.paymentMethod);
|
||||
writeNotNull('transactionId', instance.transactionId);
|
||||
val['creditsUsed'] = instance.creditsUsed;
|
||||
val['totalCredits'] = instance.totalCredits;
|
||||
writeNotNull('canceledAt', instance.canceledAt?.toIso8601String());
|
||||
writeNotNull('cancelReason', instance.cancelReason);
|
||||
writeNotNull('trialEndDate', instance.trialEndDate?.toIso8601String());
|
||||
val['isTrial'] = instance.isTrial;
|
||||
writeNotNull('createdAt', instance.createdAt?.toIso8601String());
|
||||
writeNotNull('updatedAt', instance.updatedAt?.toIso8601String());
|
||||
return val;
|
||||
}
|
||||
|
||||
const _$SubscriptionStatusEnumMap = {
|
||||
SubscriptionStatus.active: 'ACTIVE',
|
||||
SubscriptionStatus.trial: 'TRIAL',
|
||||
SubscriptionStatus.canceled: 'CANCELED',
|
||||
SubscriptionStatus.expired: 'EXPIRED',
|
||||
SubscriptionStatus.suspended: 'SUSPENDED',
|
||||
SubscriptionStatus.refunded: 'REFUNDED',
|
||||
};
|
||||
|
||||
SubscriptionStatistics _$SubscriptionStatisticsFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'SubscriptionStatistics',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = SubscriptionStatistics(
|
||||
totalPlans: $checkedConvert('totalPlans', (v) => (v as num).toInt()),
|
||||
activePlans:
|
||||
$checkedConvert('activePlans', (v) => (v as num).toInt()),
|
||||
totalSubscriptions:
|
||||
$checkedConvert('totalSubscriptions', (v) => (v as num).toInt()),
|
||||
activeSubscriptions:
|
||||
$checkedConvert('activeSubscriptions', (v) => (v as num).toInt()),
|
||||
trialSubscriptions:
|
||||
$checkedConvert('trialSubscriptions', (v) => (v as num).toInt()),
|
||||
monthlyRevenue:
|
||||
$checkedConvert('monthlyRevenue', (v) => (v as num).toDouble()),
|
||||
yearlyRevenue:
|
||||
$checkedConvert('yearlyRevenue', (v) => (v as num).toDouble()),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SubscriptionStatisticsToJson(
|
||||
SubscriptionStatistics instance) =>
|
||||
<String, dynamic>{
|
||||
'totalPlans': instance.totalPlans,
|
||||
'activePlans': instance.activePlans,
|
||||
'totalSubscriptions': instance.totalSubscriptions,
|
||||
'activeSubscriptions': instance.activeSubscriptions,
|
||||
'trialSubscriptions': instance.trialSubscriptions,
|
||||
'monthlyRevenue': instance.monthlyRevenue,
|
||||
'yearlyRevenue': instance.yearlyRevenue,
|
||||
};
|
||||
107
AINoval/lib/models/ai_context_tracking.dart
Normal file
107
AINoval/lib/models/ai_context_tracking.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
/// AI上下文追踪选项枚举
|
||||
enum AIContextTracking {
|
||||
/// 总是包含在AI上下文中
|
||||
/// 此条目被标记为全局,其信息总是呈现给AI
|
||||
always('always', '总是包含', '此条目被标记为全局,其信息总是呈现给AI'),
|
||||
|
||||
/// 检测到时包含(默认)
|
||||
/// 当在文本/选择/聊天消息中检测到此条目时,将其添加到上下文中
|
||||
detected('detected', '检测到时包含', '当在文本/选择/聊天消息中检测到此条目时,将其添加到上下文中'),
|
||||
|
||||
/// 检测到时不包含
|
||||
/// 即使检测到也不要将此条目添加到上下文中,但在被引用或手动添加为场景上下文时仍可拉入
|
||||
dontInclude('dont_include', '检测到时不包含', '即使检测到也不要将此条目添加到上下文中,但在被引用或手动添加为场景上下文时仍可拉入'),
|
||||
|
||||
/// 从不包含
|
||||
/// 此条目永远不会显示给AI,对于私人笔记或无关信息很有用
|
||||
never('never', '从不包含', '此条目永远不会显示给AI,对于私人笔记或无关信息很有用');
|
||||
|
||||
const AIContextTracking(this.value, this.displayName, this.description);
|
||||
|
||||
final String value;
|
||||
final String displayName;
|
||||
final String description;
|
||||
|
||||
/// 根据值获取枚举
|
||||
static AIContextTracking fromValue(String? value) {
|
||||
if (value == null) return detected; // 默认值
|
||||
return values.firstWhere(
|
||||
(type) => type.value == value,
|
||||
orElse: () => detected,
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取所有追踪选项的显示名称
|
||||
static List<String> get allDisplayNames {
|
||||
return values.map((type) => type.displayName).toList();
|
||||
}
|
||||
|
||||
/// 是否应该包含在AI上下文中
|
||||
bool shouldIncludeInContext({
|
||||
bool isDetected = false,
|
||||
bool isManuallyAdded = false,
|
||||
bool isReferenced = false,
|
||||
}) {
|
||||
switch (this) {
|
||||
case always:
|
||||
return true;
|
||||
case detected:
|
||||
return isDetected || isManuallyAdded || isReferenced;
|
||||
case dontInclude:
|
||||
return isManuallyAdded || isReferenced;
|
||||
case never:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 设定引用修改选项枚举
|
||||
enum SettingReferenceUpdate {
|
||||
/// 修改此设定时,自动更新所有引用此设定的地方
|
||||
update('update', '自动更新引用', '修改此设定时,自动更新所有引用此设定的地方'),
|
||||
|
||||
/// 修改此设定时,询问是否更新引用
|
||||
ask('ask', '询问是否更新', '修改此设定时,询问是否更新引用'),
|
||||
|
||||
/// 修改此设定时,不更新引用
|
||||
noUpdate('no_update', '不更新引用', '修改此设定时,不更新引用');
|
||||
|
||||
const SettingReferenceUpdate(this.value, this.displayName, this.description);
|
||||
|
||||
final String value;
|
||||
final String displayName;
|
||||
final String description;
|
||||
|
||||
/// 根据值获取枚举
|
||||
static SettingReferenceUpdate fromValue(String? value) {
|
||||
if (value == null) return ask; // 默认值
|
||||
return values.firstWhere(
|
||||
(type) => type.value == value,
|
||||
orElse: () => ask,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 名称/别名追踪选项枚举
|
||||
enum NameAliasTracking {
|
||||
/// 通过名称/别名追踪此条目
|
||||
track('track', '通过名称/别名追踪', '通过名称/别名追踪此条目'),
|
||||
|
||||
/// 不追踪此条目
|
||||
noTrack('no_track', '不追踪', '不追踪此条目');
|
||||
|
||||
const NameAliasTracking(this.value, this.displayName, this.description);
|
||||
|
||||
final String value;
|
||||
final String displayName;
|
||||
final String description;
|
||||
|
||||
/// 根据值获取枚举
|
||||
static NameAliasTracking fromValue(String? value) {
|
||||
if (value == null) return track; // 默认值
|
||||
return values.firstWhere(
|
||||
(type) => type.value == value,
|
||||
orElse: () => track,
|
||||
);
|
||||
}
|
||||
}
|
||||
408
AINoval/lib/models/ai_feature_form_config.dart
Normal file
408
AINoval/lib/models/ai_feature_form_config.dart
Normal file
@@ -0,0 +1,408 @@
|
||||
import 'package:ainoval/models/prompt_models.dart';
|
||||
|
||||
/// AI功能表单字段类型
|
||||
enum AIFormFieldType {
|
||||
instructions, // 指令字段
|
||||
length, // 长度字段 (扩写/缩写)
|
||||
style, // 重构方式字段 (重构)
|
||||
contextSelection, // 上下文选择
|
||||
smartContext, // 智能上下文开关
|
||||
promptTemplate, // 提示词模板选择
|
||||
temperature, // 温度滑动条
|
||||
topP, // Top-P滑动条
|
||||
memoryCutoff, // 记忆截断 (聊天)
|
||||
quickAccess, // 快捷访问开关
|
||||
}
|
||||
|
||||
/// 表单字段配置
|
||||
class FormFieldConfig {
|
||||
final AIFormFieldType type;
|
||||
final String title;
|
||||
final String description;
|
||||
final bool isRequired;
|
||||
final Map<String, dynamic>? options; // 用于存储字段特定选项
|
||||
|
||||
const FormFieldConfig({
|
||||
required this.type,
|
||||
required this.title,
|
||||
required this.description,
|
||||
this.isRequired = false,
|
||||
this.options,
|
||||
});
|
||||
}
|
||||
|
||||
/// AI功能表单配置
|
||||
class AIFeatureFormConfig {
|
||||
static const Map<AIFeatureType, List<FormFieldConfig>> _configs = {
|
||||
// 文本扩写
|
||||
AIFeatureType.textExpansion: [
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.instructions,
|
||||
title: '指令',
|
||||
description: '应该如何扩写文本?',
|
||||
options: {
|
||||
'placeholder': 'e.g. 描述设定',
|
||||
'presets': [
|
||||
{'id': 'descriptive', 'title': '描述性扩写', 'content': '请为这段文本添加更详细的描述,包括环境、感官细节和人物心理描写。'},
|
||||
{'id': 'dialogue', 'title': '对话扩写', 'content': '请为这段文本添加更多的对话和人物互动,展现人物性格。'},
|
||||
{'id': 'action', 'title': '动作扩写', 'content': '请为这段文本添加更多的动作描写和情节发展。'},
|
||||
],
|
||||
},
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.length,
|
||||
title: '长度',
|
||||
description: '扩写后的文本应该多长?',
|
||||
options: {
|
||||
'radioOptions': [
|
||||
{'value': 'double', 'label': '双倍'},
|
||||
{'value': 'triple', 'label': '三倍'},
|
||||
],
|
||||
'placeholder': 'e.g. 400 words',
|
||||
},
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.contextSelection,
|
||||
title: '附加上下文',
|
||||
description: '为AI提供的任何额外信息',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.smartContext,
|
||||
title: '智能上下文',
|
||||
description: '使用AI自动检索相关背景信息,提升生成质量',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.promptTemplate,
|
||||
title: '关联提示词模板',
|
||||
description: '选择要关联的提示词模板(可选)',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.temperature,
|
||||
title: '温度',
|
||||
description: '控制生成内容的创造性',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.topP,
|
||||
title: 'Top-P',
|
||||
description: '控制生成内容的多样性',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.quickAccess,
|
||||
title: '快捷访问',
|
||||
description: '是否在功能对话框中显示此预设',
|
||||
),
|
||||
],
|
||||
|
||||
// 文本缩写
|
||||
AIFeatureType.textSummary: [
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.length,
|
||||
title: '长度',
|
||||
description: '缩短后的文本应该多长?',
|
||||
isRequired: true,
|
||||
options: {
|
||||
'radioOptions': [
|
||||
{'value': 'half', 'label': '一半'},
|
||||
{'value': 'quarter', 'label': '四分之一'},
|
||||
{'value': 'paragraph', 'label': '单段落'},
|
||||
],
|
||||
'placeholder': 'e.g. 100 words',
|
||||
},
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.instructions,
|
||||
title: '指令',
|
||||
description: '为AI提供的任何(可选)额外指令和角色',
|
||||
options: {
|
||||
'placeholder': 'e.g. You are a...',
|
||||
'presets': [
|
||||
{'id': 'brief', 'title': '简洁摘要', 'content': '请将这段文本总结为简洁的要点。'},
|
||||
{'id': 'detailed', 'title': '详细摘要', 'content': '请提供详细的摘要,保留关键细节。'},
|
||||
],
|
||||
},
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.contextSelection,
|
||||
title: '附加上下文',
|
||||
description: '为AI提供的任何额外信息',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.smartContext,
|
||||
title: '智能上下文',
|
||||
description: '使用AI自动检索相关背景信息,提升缩写质量',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.promptTemplate,
|
||||
title: '关联提示词模板',
|
||||
description: '选择要关联的提示词模板(可选)',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.temperature,
|
||||
title: '温度',
|
||||
description: '控制生成内容的创造性',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.topP,
|
||||
title: 'Top-P',
|
||||
description: '控制生成内容的多样性',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.quickAccess,
|
||||
title: '快捷访问',
|
||||
description: '是否在功能对话框中显示此预设',
|
||||
),
|
||||
],
|
||||
|
||||
// 文本重构
|
||||
AIFeatureType.textRefactor: [
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.instructions,
|
||||
title: '指令',
|
||||
description: '应该如何重构文本?',
|
||||
options: {
|
||||
'placeholder': 'e.g. 重写以提高清晰度',
|
||||
'presets': [
|
||||
{'id': 'dramatic', 'title': '增强戏剧性', 'content': '让这段文字更具戏剧性和冲突感,增强情节张力。'},
|
||||
{'id': 'style', 'title': '改变风格', 'content': '请将这段文字改写为更优雅/现代/古典的文学风格。'},
|
||||
{'id': 'pov', 'title': '转换视角', 'content': '请将这段文字从第一人称改写为第三人称(或相反)。'},
|
||||
{'id': 'mood', 'title': '调整情绪', 'content': '请调整这段文字的情绪氛围,使其更加轻松/严肃/神秘/温馨。'},
|
||||
],
|
||||
},
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.style,
|
||||
title: '重构方式',
|
||||
description: '重点关注哪个方面?',
|
||||
options: {
|
||||
'radioOptions': [
|
||||
{'value': 'clarity', 'label': '清晰度'},
|
||||
{'value': 'flow', 'label': '流畅性'},
|
||||
{'value': 'tone', 'label': '语调'},
|
||||
],
|
||||
'placeholder': 'e.g. 更加正式',
|
||||
},
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.contextSelection,
|
||||
title: '附加上下文',
|
||||
description: '为AI提供的任何额外信息',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.smartContext,
|
||||
title: '智能上下文',
|
||||
description: '使用AI自动检索相关背景信息,提升重构质量',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.promptTemplate,
|
||||
title: '关联提示词模板',
|
||||
description: '选择要关联的提示词模板(可选)',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.temperature,
|
||||
title: '温度',
|
||||
description: '控制生成内容的创造性',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.topP,
|
||||
title: 'Top-P',
|
||||
description: '控制生成内容的多样性',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.quickAccess,
|
||||
title: '快捷访问',
|
||||
description: '是否在功能对话框中显示此预设',
|
||||
),
|
||||
],
|
||||
|
||||
// AI聊天
|
||||
AIFeatureType.aiChat: [
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.instructions,
|
||||
title: 'Instructions',
|
||||
description: 'Any (optional) additional instructions and roles for the AI',
|
||||
options: {
|
||||
'placeholder': 'e.g. You are a...',
|
||||
},
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.contextSelection,
|
||||
title: 'Additional Context',
|
||||
description: 'Any additional information to provide to the AI',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.smartContext,
|
||||
title: 'Smart Context',
|
||||
description: 'Use AI to automatically retrieve relevant background information',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.promptTemplate,
|
||||
title: '关联提示词模板',
|
||||
description: '选择要关联的提示词模板(可选)',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.temperature,
|
||||
title: '温度',
|
||||
description: '控制生成内容的创造性',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.topP,
|
||||
title: 'Top-P',
|
||||
description: '控制生成内容的多样性',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.memoryCutoff,
|
||||
title: 'Memory Cutoff',
|
||||
description: 'Specify a maximum number of message pairs to be sent to the AI. Any messages exceeding this limit will be ignored.',
|
||||
options: {
|
||||
'radioOptions': [
|
||||
{'value': 14, 'label': '14 (Default)'},
|
||||
{'value': 28, 'label': '28'},
|
||||
{'value': 48, 'label': '48'},
|
||||
{'value': 64, 'label': '64'},
|
||||
],
|
||||
'placeholder': 'e.g. 24',
|
||||
},
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.quickAccess,
|
||||
title: '快捷访问',
|
||||
description: '是否在功能对话框中显示此预设',
|
||||
),
|
||||
],
|
||||
|
||||
// 🚀 新增:场景节拍生成
|
||||
AIFeatureType.sceneBeatGeneration: [
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.instructions,
|
||||
title: '指令',
|
||||
description: '为AI提供的场景节拍生成指令',
|
||||
options: {
|
||||
'placeholder': 'e.g. 续写故事,创造一个转折点...',
|
||||
'presets': [
|
||||
{'id': 'turning_point', 'title': '转折点', 'content': '创造一个重要的转折点,改变故事走向。'},
|
||||
{'id': 'character_growth', 'title': '角色成长', 'content': '展现角色的内心成长和变化。'},
|
||||
{'id': 'conflict_escalation', 'title': '冲突升级', 'content': '加剧现有冲突,增强戏剧张力。'},
|
||||
{'id': 'revelation', 'title': '重要揭示', 'content': '揭示重要信息或秘密,推动情节发展。'},
|
||||
],
|
||||
},
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.length,
|
||||
title: '长度',
|
||||
description: '生成内容的字数',
|
||||
isRequired: true,
|
||||
options: {
|
||||
'radioOptions': [
|
||||
{'value': '200', 'label': '200字'},
|
||||
{'value': '400', 'label': '400字'},
|
||||
{'value': '600', 'label': '600字'},
|
||||
],
|
||||
'placeholder': 'e.g. 500',
|
||||
},
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.contextSelection,
|
||||
title: '附加上下文',
|
||||
description: '为AI提供的任何额外信息',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.smartContext,
|
||||
title: '智能上下文',
|
||||
description: '使用AI自动检索相关背景信息,提升生成质量',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.promptTemplate,
|
||||
title: '关联提示词模板',
|
||||
description: '选择要关联的提示词模板(可选)',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.temperature,
|
||||
title: '温度',
|
||||
description: '控制生成内容的创造性',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.topP,
|
||||
title: 'Top-P',
|
||||
description: '控制生成内容的多样性',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.quickAccess,
|
||||
title: '快捷访问',
|
||||
description: '是否在功能对话框中显示此预设',
|
||||
),
|
||||
],
|
||||
|
||||
// 🚀 新增:写作编排(大纲/章节/组合)
|
||||
AIFeatureType.novelCompose: [
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.instructions,
|
||||
title: '指令',
|
||||
description: '为AI提供写作编排的总体目标(如风格、体裁、读者定位等)',
|
||||
options: {
|
||||
'placeholder': 'e.g. 悬疑+家庭剧的现代都市小说,目标读者18-35,节奏偏快',
|
||||
},
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.contextSelection,
|
||||
title: '附加上下文',
|
||||
description: '为AI提供的任何额外信息(设定、摘要、章节等)',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.smartContext,
|
||||
title: '智能上下文',
|
||||
description: '使用AI自动检索相关背景信息,提升编排质量',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.promptTemplate,
|
||||
title: '关联提示词模板',
|
||||
description: '选择要关联的提示词模板(可选)',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.temperature,
|
||||
title: '温度',
|
||||
description: '控制生成内容的创造性',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.topP,
|
||||
title: 'Top-P',
|
||||
description: '控制生成内容的多样性',
|
||||
),
|
||||
const FormFieldConfig(
|
||||
type: AIFormFieldType.quickAccess,
|
||||
title: '快捷访问',
|
||||
description: '是否在功能对话框中显示此预设',
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
/// 获取指定AI功能类型的表单配置
|
||||
static List<FormFieldConfig> getFormConfig(AIFeatureType featureType) {
|
||||
return _configs[featureType] ?? [];
|
||||
}
|
||||
|
||||
/// 获取指定AI功能类型的表单配置(通过字符串)
|
||||
static List<FormFieldConfig> getFormConfigByString(String featureTypeString) {
|
||||
try {
|
||||
final featureType = AIFeatureTypeHelper.fromApiString(featureTypeString.toUpperCase());
|
||||
return getFormConfig(featureType);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查指定功能类型是否包含某个字段
|
||||
static bool hasField(AIFeatureType featureType, AIFormFieldType fieldType) {
|
||||
final config = getFormConfig(featureType);
|
||||
return config.any((field) => field.type == fieldType);
|
||||
}
|
||||
|
||||
/// 获取指定功能类型的指定字段配置
|
||||
static FormFieldConfig? getFieldConfig(AIFeatureType featureType, AIFormFieldType fieldType) {
|
||||
final config = getFormConfig(featureType);
|
||||
try {
|
||||
return config.firstWhere((field) => field.type == fieldType);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
123
AINoval/lib/models/ai_model_group.dart
Normal file
123
AINoval/lib/models/ai_model_group.dart
Normal file
@@ -0,0 +1,123 @@
|
||||
import 'package:ainoval/models/model_info.dart'; // Import ModelInfo
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// AI模型分组模型,用于UI显示
|
||||
@immutable
|
||||
class AIModelGroup {
|
||||
const AIModelGroup({
|
||||
required this.provider,
|
||||
required this.groups,
|
||||
});
|
||||
|
||||
final String provider;
|
||||
final List<ModelPrefixGroup> groups;
|
||||
|
||||
/// 从 ModelInfo 列表创建分组
|
||||
factory AIModelGroup.fromModelInfoList(String provider, List<ModelInfo> models) {
|
||||
final Map<String, List<ModelInfo>> groupedModels = {};
|
||||
|
||||
for (final modelInfo in models) {
|
||||
String prefix;
|
||||
// Use model ID for prefix extraction
|
||||
final modelId = modelInfo.id;
|
||||
if (modelId.contains('/')) {
|
||||
prefix = modelId.split('/').first;
|
||||
} else if (modelId.contains(':')) {
|
||||
prefix = modelId.split(':').first;
|
||||
} else if (modelId.contains('-')) {
|
||||
final parts = modelId.split('-');
|
||||
prefix = parts.first;
|
||||
} else {
|
||||
prefix = modelId;
|
||||
}
|
||||
|
||||
if (!groupedModels.containsKey(prefix)) {
|
||||
groupedModels[prefix] = [];
|
||||
}
|
||||
groupedModels[prefix]!.add(modelInfo);
|
||||
}
|
||||
|
||||
final groups = groupedModels.entries
|
||||
.map((entry) => ModelPrefixGroup(
|
||||
prefix: entry.key,
|
||||
// Pass ModelInfo list to ModelPrefixGroup constructor
|
||||
modelsInfo: entry.value,
|
||||
))
|
||||
.toList();
|
||||
|
||||
groups.sort((a, b) => a.prefix.compareTo(b.prefix));
|
||||
|
||||
return AIModelGroup(
|
||||
provider: provider,
|
||||
groups: groups,
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取所有模型的平铺列表
|
||||
List<ModelInfo> get allModelsInfo {
|
||||
final List<ModelInfo> result = [];
|
||||
for (final group in groups) {
|
||||
result.addAll(group.modelsInfo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is AIModelGroup &&
|
||||
other.provider == provider &&
|
||||
_listEquals(other.groups, groups);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => provider.hashCode ^ Object.hashAll(groups);
|
||||
|
||||
// 辅助方法:比较两个列表是否相等
|
||||
bool _listEquals<T>(List<T>? a, List<T>? b) {
|
||||
if (a == null) return b == null;
|
||||
if (b == null || a.length != b.length) return false;
|
||||
for (int i = 0; i < a.length; i++) {
|
||||
if (a[i] != b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// 按前缀分组的模型
|
||||
@immutable
|
||||
class ModelPrefixGroup {
|
||||
const ModelPrefixGroup({
|
||||
required this.prefix,
|
||||
required this.modelsInfo, // Change from models (List<String>)
|
||||
});
|
||||
|
||||
final String prefix;
|
||||
final List<ModelInfo> modelsInfo; // Store ModelInfo
|
||||
|
||||
// Keep models getter for backward compatibility or UI that needs strings?
|
||||
List<String> get models => modelsInfo.map((info) => info.id).toList();
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is ModelPrefixGroup &&
|
||||
other.prefix == prefix &&
|
||||
_listEquals(other.modelsInfo, modelsInfo); // Compare ModelInfo lists
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => prefix.hashCode ^ Object.hashAll(modelsInfo);
|
||||
|
||||
// 辅助方法:比较两个列表是否相等
|
||||
bool _listEquals<T>(List<T>? a, List<T>? b) {
|
||||
if (a == null) return b == null;
|
||||
if (b == null || a.length != b.length) return false;
|
||||
for (int i = 0; i < a.length; i++) {
|
||||
if (a[i] != b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
680
AINoval/lib/models/ai_request_models.dart
Normal file
680
AINoval/lib/models/ai_request_models.dart
Normal file
@@ -0,0 +1,680 @@
|
||||
import 'package:ainoval/models/context_selection_models.dart';
|
||||
import 'package:ainoval/models/user_ai_model_config_model.dart';
|
||||
import 'package:ainoval/utils/date_time_parser.dart';
|
||||
|
||||
/// AI请求类型枚举
|
||||
enum AIRequestType {
|
||||
chat('AI_CHAT', '聊天对话'),
|
||||
expansion('TEXT_EXPANSION', '扩写文本'),
|
||||
summary('TEXT_SUMMARY', '缩写文本'),
|
||||
sceneSummary('SCENE_TO_SUMMARY', '场景摘要'),
|
||||
refactor('TEXT_REFACTOR', '重构文本'),
|
||||
generation('NOVEL_GENERATION', '内容生成'),
|
||||
sceneBeat('SCENE_BEAT_GENERATION', '场景节拍生成'),
|
||||
novelCompose('NOVEL_COMPOSE', '设定编排');
|
||||
|
||||
const AIRequestType(this.value, this.displayName);
|
||||
|
||||
final String value;
|
||||
final String displayName;
|
||||
}
|
||||
|
||||
/// 通用AI请求模型
|
||||
class UniversalAIRequest {
|
||||
const UniversalAIRequest({
|
||||
required this.requestType,
|
||||
required this.userId,
|
||||
this.sessionId,
|
||||
this.novelId,
|
||||
this.chapterId,
|
||||
this.sceneId,
|
||||
this.settingSessionId,
|
||||
this.modelConfig,
|
||||
this.prompt,
|
||||
this.instructions,
|
||||
this.selectedText,
|
||||
this.contextSelections,
|
||||
this.enableSmartContext = false,
|
||||
this.parameters = const {},
|
||||
this.metadata = const {},
|
||||
});
|
||||
|
||||
/// 请求类型
|
||||
final AIRequestType requestType;
|
||||
|
||||
/// 用户ID
|
||||
final String userId;
|
||||
|
||||
/// 会话ID(聊天对话时必填)
|
||||
final String? sessionId;
|
||||
|
||||
/// 小说ID
|
||||
final String? novelId;
|
||||
|
||||
/// 章节ID(用于上下文提供器)
|
||||
final String? chapterId;
|
||||
|
||||
/// 场景ID(用于上下文提供器)
|
||||
final String? sceneId;
|
||||
|
||||
/// 设定生成会话ID(用于设定编排/写作编排场景)
|
||||
final String? settingSessionId;
|
||||
|
||||
/// 模型配置
|
||||
final UserAIModelConfigModel? modelConfig;
|
||||
|
||||
/// 主要提示内容(用户输入的消息或待处理的文本)
|
||||
final String? prompt;
|
||||
|
||||
/// 指令内容(AI执行任务的具体指导)
|
||||
final String? instructions;
|
||||
|
||||
/// 选中的文本(扩写、缩写、重构时使用)
|
||||
final String? selectedText;
|
||||
|
||||
/// 上下文选择数据
|
||||
final ContextSelectionData? contextSelections;
|
||||
|
||||
/// 是否启用智能上下文(RAG检索)
|
||||
final bool enableSmartContext;
|
||||
|
||||
/// 请求参数(温度、最大token等)
|
||||
final Map<String, dynamic> parameters;
|
||||
|
||||
/// 元数据(其他附加信息)
|
||||
final Map<String, dynamic> metadata;
|
||||
|
||||
/// 复制方法
|
||||
UniversalAIRequest copyWith({
|
||||
AIRequestType? requestType,
|
||||
String? userId,
|
||||
String? sessionId,
|
||||
String? novelId,
|
||||
String? chapterId,
|
||||
String? sceneId,
|
||||
String? settingSessionId,
|
||||
UserAIModelConfigModel? modelConfig,
|
||||
String? prompt,
|
||||
String? instructions,
|
||||
String? selectedText,
|
||||
ContextSelectionData? contextSelections,
|
||||
bool? enableSmartContext,
|
||||
Map<String, dynamic>? parameters,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) {
|
||||
return UniversalAIRequest(
|
||||
requestType: requestType ?? this.requestType,
|
||||
userId: userId ?? this.userId,
|
||||
sessionId: sessionId ?? this.sessionId,
|
||||
novelId: novelId ?? this.novelId,
|
||||
chapterId: chapterId ?? this.chapterId,
|
||||
sceneId: sceneId ?? this.sceneId,
|
||||
settingSessionId: settingSessionId ?? this.settingSessionId,
|
||||
modelConfig: modelConfig ?? this.modelConfig,
|
||||
prompt: prompt ?? this.prompt,
|
||||
instructions: instructions ?? this.instructions,
|
||||
selectedText: selectedText ?? this.selectedText,
|
||||
contextSelections: contextSelections ?? this.contextSelections,
|
||||
enableSmartContext: enableSmartContext ?? this.enableSmartContext,
|
||||
parameters: parameters ?? this.parameters,
|
||||
metadata: metadata ?? this.metadata,
|
||||
);
|
||||
}
|
||||
|
||||
/// 转换为API请求的JSON格式
|
||||
Map<String, dynamic> toApiJson() {
|
||||
final Map<String, dynamic> json = {
|
||||
'requestType': requestType.value,
|
||||
'userId': userId,
|
||||
'enableSmartContext': enableSmartContext,
|
||||
};
|
||||
|
||||
// 添加可选字段
|
||||
if (sessionId != null) json['sessionId'] = sessionId;
|
||||
if (novelId != null) json['novelId'] = novelId;
|
||||
if (chapterId != null) json['chapterId'] = chapterId;
|
||||
if (sceneId != null) json['sceneId'] = sceneId;
|
||||
if (settingSessionId != null) json['settingSessionId'] = settingSessionId;
|
||||
if (prompt != null) json['prompt'] = prompt;
|
||||
if (instructions != null) json['instructions'] = instructions;
|
||||
if (selectedText != null) json['selectedText'] = selectedText;
|
||||
|
||||
// 模型配置
|
||||
if (modelConfig != null) {
|
||||
json['modelName'] = modelConfig!.modelName;
|
||||
json['modelProvider'] = modelConfig!.provider;
|
||||
|
||||
final bool isPublic = metadata['isPublicModel'] == true;
|
||||
|
||||
// 仅在私有模型时发送 modelConfigId,避免公共模型被误判为私有配置查询
|
||||
if (!isPublic) {
|
||||
json['modelConfigId'] = modelConfig!.id;
|
||||
}
|
||||
|
||||
// 🚀 明确标识是否为公共模型(并传递公共配置ID)
|
||||
if (isPublic) {
|
||||
json['isPublicModel'] = true;
|
||||
if (metadata.containsKey('publicModelConfigId') && metadata['publicModelConfigId'] != null) {
|
||||
// 优先使用 publicModelConfigId(与后端期望一致)
|
||||
json['publicModelConfigId'] = metadata['publicModelConfigId'];
|
||||
}
|
||||
if (metadata.containsKey('publicModelId') && metadata['publicModelId'] != null) {
|
||||
json['publicModelId'] = metadata['publicModelId']; // 兼容旧字段
|
||||
}
|
||||
print('🔧 [UniversalAIRequest.toApiJson] 公共模型请求 - 模型: ${modelConfig!.modelName}, 提供商: ${modelConfig!.provider}, 公共模型ID: ${metadata['publicModelId'] ?? metadata['publicModelConfigId']}');
|
||||
} else {
|
||||
json['isPublicModel'] = false;
|
||||
print('🔧 [UniversalAIRequest.toApiJson] 私有模型请求 - 模型: ${modelConfig!.modelName}, 提供商: ${modelConfig!.provider}, 配置ID: ${modelConfig!.id}');
|
||||
}
|
||||
}
|
||||
|
||||
// 上下文选择
|
||||
if (contextSelections != null && contextSelections!.selectedCount > 0) {
|
||||
final contextList = contextSelections!.selectedItems.values
|
||||
.map((item) => {
|
||||
'id': item.id,
|
||||
'title': item.title,
|
||||
'type': item.type.value, // 🚀 修复:使用API值而不是displayName
|
||||
'metadata': item.metadata,
|
||||
})
|
||||
.toList();
|
||||
json['contextSelections'] = contextList;
|
||||
|
||||
// 🚀 添加调试日志
|
||||
print('🔧 [UniversalAIRequest.toApiJson] 添加上下文选择: ${contextList.length}个项目');
|
||||
for (var item in contextList) {
|
||||
print(' - ${item['type']}:${item['id']} (${item['title']})');
|
||||
}
|
||||
} else {
|
||||
print('🔧 [UniversalAIRequest.toApiJson] 没有上下文选择数据');
|
||||
}
|
||||
|
||||
// 请求参数
|
||||
json['parameters'] = {
|
||||
'temperature': parameters['temperature'] ?? 0.7,
|
||||
'maxTokens': parameters['maxTokens'] ?? 2000,
|
||||
'enableSmartContext': enableSmartContext, // 🚀 确保enableSmartContext也在parameters中
|
||||
...parameters,
|
||||
};
|
||||
|
||||
// 元数据
|
||||
if (metadata.isNotEmpty) {
|
||||
json['metadata'] = metadata;
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
/// 从JSON创建请求对象
|
||||
factory UniversalAIRequest.fromJson(Map<String, dynamic> json) {
|
||||
// 🚀 处理contextSelections字段
|
||||
ContextSelectionData? contextSelections;
|
||||
if (json['contextSelections'] != null) {
|
||||
final contextList = json['contextSelections'] as List<dynamic>;
|
||||
print('🔧 [UniversalAIRequest.fromJson] 解析contextSelections: ${contextList.length}个项目');
|
||||
|
||||
// 🚀 新增:检查是否需要过滤预设模板上下文
|
||||
final isPresetTemplate = json['metadata']?['isPresetTemplate'] == true ||
|
||||
json['source'] == 'preset_template' ||
|
||||
contextList.any((item) => item['metadata']?['isHardcoded'] == true);
|
||||
|
||||
if (isPresetTemplate) {
|
||||
print('🔧 [UniversalAIRequest.fromJson] 检测到预设模板,启用上下文过滤');
|
||||
}
|
||||
|
||||
// 将已选择的项目转换为ContextSelectionItem,并标记为已选择
|
||||
final selectedItems = <String, ContextSelectionItem>{};
|
||||
final availableItems = <ContextSelectionItem>[];
|
||||
final flatItems = <String, ContextSelectionItem>{};
|
||||
|
||||
for (var itemData in contextList) {
|
||||
final contextType = itemData['type'] as String?;
|
||||
|
||||
// 🚀 预设模板上下文过滤:只保留硬编码的上下文类型
|
||||
if (isPresetTemplate && !_isHardcodedContextType(contextType)) {
|
||||
print(' 🚫 过滤掉非硬编码上下文: $contextType');
|
||||
continue;
|
||||
}
|
||||
|
||||
final item = ContextSelectionItem(
|
||||
id: itemData['id'] ?? '',
|
||||
title: itemData['title'] ?? '',
|
||||
type: ContextSelectionType.values.firstWhere(
|
||||
(type) => type.value == itemData['type'],
|
||||
orElse: () => ContextSelectionType.fullNovelText,
|
||||
),
|
||||
metadata: Map<String, dynamic>.from(itemData['metadata'] ?? {}),
|
||||
parentId: itemData['parentId'],
|
||||
selectionState: SelectionState.fullySelected, // 标记为已选择
|
||||
);
|
||||
|
||||
selectedItems[item.id] = item;
|
||||
availableItems.add(item);
|
||||
flatItems[item.id] = item;
|
||||
|
||||
print(' ✅ ${item.type.displayName}:${item.id} (${item.title})');
|
||||
}
|
||||
|
||||
// 创建ContextSelectionData,包含选择状态
|
||||
contextSelections = ContextSelectionData(
|
||||
novelId: json['novelId'] ?? '',
|
||||
selectedItems: selectedItems,
|
||||
availableItems: availableItems,
|
||||
flatItems: flatItems,
|
||||
);
|
||||
|
||||
if (isPresetTemplate) {
|
||||
print('🔧 [UniversalAIRequest.fromJson] 预设模板上下文过滤完成: ${contextSelections.selectedCount}个硬编码项目');
|
||||
} else {
|
||||
print('🔧 [UniversalAIRequest.fromJson] 创建ContextSelectionData: ${contextSelections.selectedCount}个已选择项目');
|
||||
}
|
||||
}
|
||||
|
||||
// 🚀 智能获取enableSmartContext:优先从顶级字段获取,回退到parameters中获取
|
||||
final Map<String, dynamic> parameters = Map<String, dynamic>.from(json['parameters'] ?? {});
|
||||
bool enableSmartContext = json['enableSmartContext'] ??
|
||||
parameters['enableSmartContext'] ??
|
||||
false;
|
||||
|
||||
return UniversalAIRequest(
|
||||
requestType: AIRequestType.values.firstWhere(
|
||||
(type) => type.value == json['requestType'],
|
||||
orElse: () => AIRequestType.chat,
|
||||
),
|
||||
userId: json['userId'] ?? '',
|
||||
sessionId: json['sessionId'],
|
||||
novelId: json['novelId'],
|
||||
chapterId: json['chapterId'],
|
||||
sceneId: json['sceneId'],
|
||||
settingSessionId: json['settingSessionId'],
|
||||
prompt: json['prompt'],
|
||||
instructions: json['instructions'],
|
||||
selectedText: json['selectedText'],
|
||||
contextSelections: contextSelections,
|
||||
enableSmartContext: enableSmartContext,
|
||||
parameters: parameters,
|
||||
metadata: Map<String, dynamic>.from(json['metadata'] ?? {}),
|
||||
);
|
||||
}
|
||||
|
||||
/// 🚀 新增:判断是否为硬编码的预设模板上下文类型
|
||||
static bool _isHardcodedContextType(String? contextType) {
|
||||
if (contextType == null) return false;
|
||||
|
||||
// 定义预设模板允许的硬编码上下文类型
|
||||
const hardcodedTypes = {
|
||||
// 核心文本上下文
|
||||
'full_novel_text', // 全文文本
|
||||
'full_outline', // 完整大纲
|
||||
'novel_basic_info', // 基本信息
|
||||
|
||||
// 前五章相关
|
||||
'recent_chapters_content', // 前五章内容
|
||||
'recent_chapters_summary', // 前五章摘要
|
||||
|
||||
// 结构化上下文
|
||||
'settings', // 设定
|
||||
'snippets', // 片段
|
||||
|
||||
// 当前上下文
|
||||
'chapters', // 章节(当前章节)
|
||||
'scenes', // 场景(当前场景)
|
||||
|
||||
// 世界观相关
|
||||
'setting_groups', // 设定组
|
||||
'codex_entries', // 词条
|
||||
};
|
||||
|
||||
return hardcodedTypes.contains(contextType);
|
||||
}
|
||||
}
|
||||
|
||||
/// AI响应模型
|
||||
class UniversalAIResponse {
|
||||
const UniversalAIResponse({
|
||||
required this.id,
|
||||
required this.requestType,
|
||||
required this.content,
|
||||
this.finishReason,
|
||||
this.tokenUsage,
|
||||
this.model,
|
||||
this.createdAt,
|
||||
this.metadata = const {},
|
||||
});
|
||||
|
||||
/// 响应ID
|
||||
final String id;
|
||||
|
||||
/// 对应的请求类型
|
||||
final AIRequestType requestType;
|
||||
|
||||
/// 生成的内容
|
||||
final String content;
|
||||
|
||||
/// 完成原因
|
||||
final String? finishReason;
|
||||
|
||||
/// Token使用情况
|
||||
final TokenUsage? tokenUsage;
|
||||
|
||||
/// 使用的模型
|
||||
final String? model;
|
||||
|
||||
/// 创建时间
|
||||
final DateTime? createdAt;
|
||||
|
||||
/// 元数据
|
||||
final Map<String, dynamic> metadata;
|
||||
|
||||
/// 从JSON创建响应对象
|
||||
factory UniversalAIResponse.fromJson(Map<String, dynamic> json) {
|
||||
return UniversalAIResponse(
|
||||
id: json['id'] ?? '',
|
||||
requestType: AIRequestType.values.firstWhere(
|
||||
(type) => type.value == json['requestType'],
|
||||
orElse: () => AIRequestType.chat,
|
||||
),
|
||||
content: json['content'] ?? '',
|
||||
finishReason: json['finishReason'],
|
||||
tokenUsage: json['tokenUsage'] != null
|
||||
? TokenUsage.fromJson(json['tokenUsage'])
|
||||
: null,
|
||||
model: json['model'],
|
||||
createdAt: json['createdAt'] != null
|
||||
? parseBackendDateTime(json['createdAt'])
|
||||
: null,
|
||||
metadata: Map<String, dynamic>.from(json['metadata'] ?? {}),
|
||||
);
|
||||
}
|
||||
|
||||
/// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'requestType': requestType.value,
|
||||
'content': content,
|
||||
'finishReason': finishReason,
|
||||
'tokenUsage': tokenUsage?.toJson(),
|
||||
'model': model,
|
||||
'createdAt': createdAt?.toIso8601String(),
|
||||
'metadata': metadata,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Token使用情况
|
||||
class TokenUsage {
|
||||
const TokenUsage({
|
||||
this.promptTokens = 0,
|
||||
this.completionTokens = 0,
|
||||
this.totalTokens = 0,
|
||||
});
|
||||
|
||||
final int promptTokens;
|
||||
final int completionTokens;
|
||||
final int totalTokens;
|
||||
|
||||
/// 从JSON创建Token使用情况
|
||||
factory TokenUsage.fromJson(Map<String, dynamic> json) {
|
||||
return TokenUsage(
|
||||
promptTokens: json['promptTokens'] ?? 0,
|
||||
completionTokens: json['completionTokens'] ?? 0,
|
||||
totalTokens: json['totalTokens'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'promptTokens': promptTokens,
|
||||
'completionTokens': completionTokens,
|
||||
'totalTokens': totalTokens,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 通用AI预览响应模型
|
||||
class UniversalAIPreviewResponse {
|
||||
const UniversalAIPreviewResponse({
|
||||
required this.preview,
|
||||
required this.systemPrompt,
|
||||
required this.userPrompt,
|
||||
this.context,
|
||||
this.estimatedTokens,
|
||||
this.modelName,
|
||||
this.modelProvider,
|
||||
this.modelConfigId,
|
||||
});
|
||||
|
||||
/// 预览内容(完整的提示词)
|
||||
final String preview;
|
||||
|
||||
/// 系统提示词
|
||||
final String systemPrompt;
|
||||
|
||||
/// 用户提示词
|
||||
final String userPrompt;
|
||||
|
||||
/// 上下文信息
|
||||
final String? context;
|
||||
|
||||
/// 估计的Token数量
|
||||
final int? estimatedTokens;
|
||||
|
||||
/// 将要使用的模型名称
|
||||
final String? modelName;
|
||||
|
||||
/// 将要使用的模型提供商
|
||||
final String? modelProvider;
|
||||
|
||||
/// 模型配置ID
|
||||
final String? modelConfigId;
|
||||
|
||||
/// 从JSON创建预览响应
|
||||
factory UniversalAIPreviewResponse.fromJson(Map<String, dynamic> json) {
|
||||
return UniversalAIPreviewResponse(
|
||||
preview: json['preview'] ?? '',
|
||||
systemPrompt: json['systemPrompt'] ?? '',
|
||||
userPrompt: json['userPrompt'] ?? '',
|
||||
context: json['context'],
|
||||
estimatedTokens: json['estimatedTokens'],
|
||||
modelName: json['modelName'],
|
||||
modelProvider: json['modelProvider'],
|
||||
modelConfigId: json['modelConfigId'],
|
||||
);
|
||||
}
|
||||
|
||||
/// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'preview': preview,
|
||||
'systemPrompt': systemPrompt,
|
||||
'userPrompt': userPrompt,
|
||||
'context': context,
|
||||
'estimatedTokens': estimatedTokens,
|
||||
'modelName': modelName,
|
||||
'modelProvider': modelProvider,
|
||||
'modelConfigId': modelConfigId,
|
||||
};
|
||||
}
|
||||
|
||||
/// 计算系统提示词的字数
|
||||
int get systemPromptWordCount => _countWords(systemPrompt);
|
||||
|
||||
/// 计算用户提示词的字数
|
||||
int get userPromptWordCount => _countWords(userPrompt);
|
||||
|
||||
/// 计算上下文的字数
|
||||
int get contextWordCount => context != null ? _countWords(context!) : 0;
|
||||
|
||||
/// 计算总字数
|
||||
int get totalWordCount => systemPromptWordCount + userPromptWordCount + contextWordCount;
|
||||
|
||||
/// 计算字数的辅助方法
|
||||
static int _countWords(String text) {
|
||||
if (text.isEmpty) return 0;
|
||||
|
||||
// 简单的字数计算:按空格分割英文单词,中文字符直接计数
|
||||
int wordCount = 0;
|
||||
int chineseCharCount = 0;
|
||||
|
||||
// 分割文本按空格
|
||||
final words = text.split(RegExp(r'\s+'));
|
||||
|
||||
for (String word in words) {
|
||||
if (word.trim().isEmpty) continue;
|
||||
|
||||
// 计算中文字符
|
||||
for (int i = 0; i < word.length; i++) {
|
||||
final charCode = word.codeUnitAt(i);
|
||||
if (charCode >= 0x4e00 && charCode <= 0x9fff) {
|
||||
chineseCharCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 移除中文字符后计算英文单词
|
||||
final nonChineseWord = word.replaceAll(RegExp(r'[\u4e00-\u9fff]'), '');
|
||||
if (nonChineseWord.trim().isNotEmpty) {
|
||||
wordCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 中文字符每个算一个词,英文单词按原数量
|
||||
return wordCount + chineseCharCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// 扩展上下文选择类型枚举,添加value字段用于API传输
|
||||
extension ContextSelectionTypeApi on ContextSelectionType {
|
||||
String get value {
|
||||
switch (this) {
|
||||
case ContextSelectionType.fullNovelText:
|
||||
return 'full_novel_text';
|
||||
case ContextSelectionType.fullOutline:
|
||||
return 'full_outline';
|
||||
case ContextSelectionType.novelBasicInfo:
|
||||
return 'novel_basic_info';
|
||||
case ContextSelectionType.recentChaptersContent:
|
||||
return 'recent_chapters_content';
|
||||
case ContextSelectionType.recentChaptersSummary:
|
||||
return 'recent_chapters_summary';
|
||||
case ContextSelectionType.currentSceneContent:
|
||||
return 'current_scene_content';
|
||||
case ContextSelectionType.currentSceneSummary:
|
||||
return 'current_scene_summary';
|
||||
case ContextSelectionType.currentChapterContent:
|
||||
return 'current_chapter_content';
|
||||
case ContextSelectionType.currentChapterSummaries:
|
||||
return 'current_chapter_summary';
|
||||
case ContextSelectionType.previousChaptersContent:
|
||||
return 'previous_chapters_content';
|
||||
case ContextSelectionType.previousChaptersSummary:
|
||||
return 'previous_chapters_summary';
|
||||
case ContextSelectionType.contentFixedGroup:
|
||||
case ContextSelectionType.summaryFixedGroup:
|
||||
return 'group';
|
||||
case ContextSelectionType.acts:
|
||||
return 'acts';
|
||||
case ContextSelectionType.chapters:
|
||||
return 'chapters';
|
||||
case ContextSelectionType.scenes:
|
||||
return 'scenes';
|
||||
case ContextSelectionType.snippets:
|
||||
return 'snippets';
|
||||
case ContextSelectionType.settings:
|
||||
return 'settings';
|
||||
case ContextSelectionType.settingGroups:
|
||||
return 'setting_groups';
|
||||
case ContextSelectionType.settingsByType:
|
||||
return 'settings_by_type';
|
||||
case ContextSelectionType.codexEntries:
|
||||
return 'codex_entries';
|
||||
case ContextSelectionType.entriesByType:
|
||||
return 'entries_by_type';
|
||||
case ContextSelectionType.entriesByDetail:
|
||||
return 'entries_by_detail';
|
||||
case ContextSelectionType.entriesByCategory:
|
||||
return 'entries_by_category';
|
||||
case ContextSelectionType.entriesByTag:
|
||||
return 'entries_by_tag';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 🚀 积分预估响应模型
|
||||
class CostEstimationResponse {
|
||||
const CostEstimationResponse({
|
||||
required this.estimatedCost,
|
||||
required this.success,
|
||||
this.errorMessage,
|
||||
this.estimatedInputTokens,
|
||||
this.estimatedOutputTokens,
|
||||
this.costMultiplier,
|
||||
this.modelName,
|
||||
this.modelProvider,
|
||||
this.isPublicModel = false,
|
||||
this.featureType,
|
||||
});
|
||||
|
||||
/// 预估的积分成本
|
||||
final int estimatedCost;
|
||||
|
||||
/// 是否成功
|
||||
final bool success;
|
||||
|
||||
/// 错误信息
|
||||
final String? errorMessage;
|
||||
|
||||
/// 预估输入Token数量
|
||||
final int? estimatedInputTokens;
|
||||
|
||||
/// 预估输出Token数量
|
||||
final int? estimatedOutputTokens;
|
||||
|
||||
/// 成本倍率
|
||||
final double? costMultiplier;
|
||||
|
||||
/// 模型名称
|
||||
final String? modelName;
|
||||
|
||||
/// 模型提供商
|
||||
final String? modelProvider;
|
||||
|
||||
/// 是否为公共模型
|
||||
final bool isPublicModel;
|
||||
|
||||
/// 功能类型
|
||||
final String? featureType;
|
||||
|
||||
/// 从JSON创建积分预估响应
|
||||
factory CostEstimationResponse.fromJson(Map<String, dynamic> json) {
|
||||
return CostEstimationResponse(
|
||||
estimatedCost: json['estimatedCost']?.toInt() ?? 0,
|
||||
success: json['success'] ?? false,
|
||||
errorMessage: json['errorMessage'],
|
||||
estimatedInputTokens: json['estimatedInputTokens']?.toInt(),
|
||||
estimatedOutputTokens: json['estimatedOutputTokens']?.toInt(),
|
||||
costMultiplier: json['costMultiplier']?.toDouble(),
|
||||
modelName: json['modelName'],
|
||||
modelProvider: json['modelProvider'],
|
||||
isPublicModel: json['isPublicModel'] ?? false,
|
||||
featureType: json['featureType'],
|
||||
);
|
||||
}
|
||||
|
||||
/// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'estimatedCost': estimatedCost,
|
||||
'success': success,
|
||||
'errorMessage': errorMessage,
|
||||
'estimatedInputTokens': estimatedInputTokens,
|
||||
'estimatedOutputTokens': estimatedOutputTokens,
|
||||
'costMultiplier': costMultiplier,
|
||||
'modelName': modelName,
|
||||
'modelProvider': modelProvider,
|
||||
'isPublicModel': isPublicModel,
|
||||
'featureType': featureType,
|
||||
};
|
||||
}
|
||||
}
|
||||
213
AINoval/lib/models/analytics_data.dart
Normal file
213
AINoval/lib/models/analytics_data.dart
Normal file
@@ -0,0 +1,213 @@
|
||||
import 'package:ainoval/utils/date_time_parser.dart';
|
||||
|
||||
class AnalyticsData {
|
||||
final int totalWords;
|
||||
final int totalTokens;
|
||||
final int functionUsageCount;
|
||||
final int writingDays;
|
||||
final int monthlyNewWords;
|
||||
final int monthlyNewTokens;
|
||||
final int consecutiveDays;
|
||||
final String mostPopularFunction;
|
||||
|
||||
const AnalyticsData({
|
||||
required this.totalWords,
|
||||
required this.totalTokens,
|
||||
required this.functionUsageCount,
|
||||
required this.writingDays,
|
||||
required this.monthlyNewWords,
|
||||
required this.monthlyNewTokens,
|
||||
required this.consecutiveDays,
|
||||
required this.mostPopularFunction,
|
||||
});
|
||||
|
||||
factory AnalyticsData.fromJson(Map<String, dynamic> json) {
|
||||
return AnalyticsData(
|
||||
totalWords: json['totalWords'] ?? 0,
|
||||
totalTokens: json['totalTokens'] ?? 0,
|
||||
functionUsageCount: json['functionUsageCount'] ?? 0,
|
||||
writingDays: json['writingDays'] ?? 0,
|
||||
monthlyNewWords: json['monthlyNewWords'] ?? 0,
|
||||
monthlyNewTokens: json['monthlyNewTokens'] ?? 0,
|
||||
consecutiveDays: json['consecutiveDays'] ?? 0,
|
||||
mostPopularFunction: json['mostPopularFunction'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'totalWords': totalWords,
|
||||
'totalTokens': totalTokens,
|
||||
'functionUsageCount': functionUsageCount,
|
||||
'writingDays': writingDays,
|
||||
'monthlyNewWords': monthlyNewWords,
|
||||
'monthlyNewTokens': monthlyNewTokens,
|
||||
'consecutiveDays': consecutiveDays,
|
||||
'mostPopularFunction': mostPopularFunction,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TokenUsageData {
|
||||
final String date;
|
||||
final int inputTokens;
|
||||
final int outputTokens;
|
||||
final int totalTokens;
|
||||
final Map<String, int> modelTokens; // 按模型名聚合的tokens
|
||||
|
||||
const TokenUsageData({
|
||||
required this.date,
|
||||
required this.inputTokens,
|
||||
required this.outputTokens,
|
||||
required this.totalTokens,
|
||||
required this.modelTokens,
|
||||
});
|
||||
|
||||
factory TokenUsageData.fromJson(Map<String, dynamic> json) {
|
||||
return TokenUsageData(
|
||||
date: json['date'] ?? '',
|
||||
inputTokens: json['inputTokens'] ?? 0,
|
||||
outputTokens: json['outputTokens'] ?? 0,
|
||||
totalTokens: json['totalTokens'] ?? 0,
|
||||
modelTokens: Map<String, int>.from(json['modelTokens'] ?? {}),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'date': date,
|
||||
'inputTokens': inputTokens,
|
||||
'outputTokens': outputTokens,
|
||||
'totalTokens': totalTokens,
|
||||
'modelTokens': modelTokens,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class FunctionUsageData {
|
||||
final String name;
|
||||
final int value;
|
||||
final double growth; // 增长率百分比
|
||||
|
||||
const FunctionUsageData({
|
||||
required this.name,
|
||||
required this.value,
|
||||
required this.growth,
|
||||
});
|
||||
|
||||
factory FunctionUsageData.fromJson(Map<String, dynamic> json) {
|
||||
return FunctionUsageData(
|
||||
name: json['name'] ?? '',
|
||||
value: json['value'] ?? 0,
|
||||
growth: (json['growth'] ?? 0.0).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'value': value,
|
||||
'growth': growth,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ModelUsageData {
|
||||
final String modelName;
|
||||
final int percentage;
|
||||
final int totalTokens;
|
||||
final String color;
|
||||
|
||||
const ModelUsageData({
|
||||
required this.modelName,
|
||||
required this.percentage,
|
||||
required this.totalTokens,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
factory ModelUsageData.fromJson(Map<String, dynamic> json) {
|
||||
return ModelUsageData(
|
||||
modelName: json['modelName'] ?? '',
|
||||
percentage: json['percentage'] ?? 0,
|
||||
totalTokens: json['totalTokens'] ?? 0,
|
||||
color: json['color'] ?? '#000000',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'modelName': modelName,
|
||||
'percentage': percentage,
|
||||
'totalTokens': totalTokens,
|
||||
'color': color,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TokenUsageRecord {
|
||||
final String id;
|
||||
final DateTime timestamp;
|
||||
final int inputTokens;
|
||||
final int outputTokens;
|
||||
final String model;
|
||||
final String taskType;
|
||||
final double cost;
|
||||
|
||||
const TokenUsageRecord({
|
||||
required this.id,
|
||||
required this.timestamp,
|
||||
required this.inputTokens,
|
||||
required this.outputTokens,
|
||||
required this.model,
|
||||
required this.taskType,
|
||||
required this.cost,
|
||||
});
|
||||
|
||||
factory TokenUsageRecord.fromJson(Map<String, dynamic> json) {
|
||||
return TokenUsageRecord(
|
||||
id: json['id'] ?? '',
|
||||
timestamp: parseBackendDateTime(json['timestamp']),
|
||||
inputTokens: json['inputTokens'] ?? 0,
|
||||
outputTokens: json['outputTokens'] ?? 0,
|
||||
model: json['model'] ?? '',
|
||||
taskType: json['taskType'] ?? '',
|
||||
cost: (json['cost'] ?? 0.0).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'inputTokens': inputTokens,
|
||||
'outputTokens': outputTokens,
|
||||
'model': model,
|
||||
'taskType': taskType,
|
||||
'cost': cost,
|
||||
};
|
||||
}
|
||||
|
||||
int get totalTokens => inputTokens + outputTokens;
|
||||
}
|
||||
|
||||
enum AnalyticsViewMode {
|
||||
daily,
|
||||
monthly,
|
||||
cumulative,
|
||||
range,
|
||||
}
|
||||
|
||||
extension AnalyticsViewModeExtension on AnalyticsViewMode {
|
||||
String get displayName {
|
||||
switch (this) {
|
||||
case AnalyticsViewMode.daily:
|
||||
return '按天';
|
||||
case AnalyticsViewMode.monthly:
|
||||
return '按月';
|
||||
case AnalyticsViewMode.cumulative:
|
||||
return '累计';
|
||||
case AnalyticsViewMode.range:
|
||||
return '日期范围';
|
||||
}
|
||||
}
|
||||
}
|
||||
67
AINoval/lib/models/api/editor_dtos.dart
Normal file
67
AINoval/lib/models/api/editor_dtos.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
/// 场景摘要生成请求 DTO
|
||||
class SummarizeSceneRequest {
|
||||
final String? additionalInstructions;
|
||||
|
||||
SummarizeSceneRequest({
|
||||
this.additionalInstructions,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
if (additionalInstructions != null) 'additionalInstructions': additionalInstructions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 场景摘要生成响应 DTO
|
||||
class SummarizeSceneResponse {
|
||||
final String summary;
|
||||
|
||||
SummarizeSceneResponse({
|
||||
required this.summary,
|
||||
});
|
||||
|
||||
factory SummarizeSceneResponse.fromJson(Map<String, dynamic> json) {
|
||||
return SummarizeSceneResponse(
|
||||
summary: json['summary'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 从摘要生成场景请求 DTO
|
||||
class GenerateSceneFromSummaryRequest {
|
||||
final String summary;
|
||||
final String? chapterId;
|
||||
final String? additionalInstructions;
|
||||
|
||||
GenerateSceneFromSummaryRequest({
|
||||
required this.summary,
|
||||
this.chapterId,
|
||||
this.additionalInstructions,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'summary': summary,
|
||||
if (chapterId != null) 'chapterId': chapterId,
|
||||
if (additionalInstructions != null) 'additionalInstructions': additionalInstructions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 从摘要生成场景响应 DTO
|
||||
class GenerateSceneFromSummaryResponse {
|
||||
final String content;
|
||||
|
||||
GenerateSceneFromSummaryResponse({
|
||||
required this.content,
|
||||
});
|
||||
|
||||
factory GenerateSceneFromSummaryResponse.fromJson(Map<String, dynamic> json) {
|
||||
return GenerateSceneFromSummaryResponse(
|
||||
content: json['content'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
207
AINoval/lib/models/app_registration_config.dart
Normal file
207
AINoval/lib/models/app_registration_config.dart
Normal file
@@ -0,0 +1,207 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// 应用注册配置
|
||||
/// 管理注册功能的开关和设置
|
||||
class AppRegistrationConfig {
|
||||
static const String _phoneRegistrationEnabledKey = 'phone_registration_enabled';
|
||||
static const String _emailRegistrationEnabledKey = 'email_registration_enabled';
|
||||
static const String _requireVerificationKey = 'require_verification';
|
||||
static const String _quickRegistrationEnabledKey = 'quick_registration_enabled';
|
||||
|
||||
// 默认配置(MVP:仅快捷注册)
|
||||
static const bool _defaultPhoneRegistrationEnabled = false; // 关闭手机注册
|
||||
static const bool _defaultEmailRegistrationEnabled = false; // 关闭邮箱注册
|
||||
static const bool _defaultRequireVerification = false; // 关闭验证码
|
||||
static const bool _defaultQuickRegistrationEnabled = true; // 开启快捷注册
|
||||
|
||||
// 缓存配置
|
||||
static bool? _cachedPhoneRegistrationEnabled;
|
||||
static bool? _cachedEmailRegistrationEnabled;
|
||||
static bool? _cachedRequireVerification;
|
||||
static bool? _cachedQuickRegistrationEnabled;
|
||||
|
||||
/// 获取是否启用快捷注册
|
||||
static Future<bool> isQuickRegistrationEnabled() async {
|
||||
if (_cachedQuickRegistrationEnabled != null) {
|
||||
return _cachedQuickRegistrationEnabled!;
|
||||
}
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_cachedQuickRegistrationEnabled = prefs.getBool(_quickRegistrationEnabledKey) ?? _defaultQuickRegistrationEnabled;
|
||||
return _cachedQuickRegistrationEnabled!;
|
||||
}
|
||||
|
||||
/// 设置是否启用快捷注册
|
||||
static Future<void> setQuickRegistrationEnabled(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_quickRegistrationEnabledKey, enabled);
|
||||
_cachedQuickRegistrationEnabled = enabled;
|
||||
}
|
||||
|
||||
/// 获取是否启用手机注册
|
||||
static Future<bool> isPhoneRegistrationEnabled() async {
|
||||
if (_cachedPhoneRegistrationEnabled != null) {
|
||||
return _cachedPhoneRegistrationEnabled!;
|
||||
}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_cachedPhoneRegistrationEnabled = prefs.getBool(_phoneRegistrationEnabledKey) ?? _defaultPhoneRegistrationEnabled;
|
||||
return _cachedPhoneRegistrationEnabled!;
|
||||
}
|
||||
|
||||
/// 设置是否启用手机注册
|
||||
static Future<void> setPhoneRegistrationEnabled(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_phoneRegistrationEnabledKey, enabled);
|
||||
_cachedPhoneRegistrationEnabled = enabled;
|
||||
}
|
||||
|
||||
/// 获取是否启用邮箱注册
|
||||
static Future<bool> isEmailRegistrationEnabled() async {
|
||||
if (_cachedEmailRegistrationEnabled != null) {
|
||||
return _cachedEmailRegistrationEnabled!;
|
||||
}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_cachedEmailRegistrationEnabled = prefs.getBool(_emailRegistrationEnabledKey) ?? _defaultEmailRegistrationEnabled;
|
||||
return _cachedEmailRegistrationEnabled!;
|
||||
}
|
||||
|
||||
/// 设置是否启用邮箱注册
|
||||
static Future<void> setEmailRegistrationEnabled(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_emailRegistrationEnabledKey, enabled);
|
||||
_cachedEmailRegistrationEnabled = enabled;
|
||||
}
|
||||
|
||||
/// 获取是否需要验证
|
||||
static Future<bool> isVerificationRequired() async {
|
||||
if (_cachedRequireVerification != null) {
|
||||
return _cachedRequireVerification!;
|
||||
}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_cachedRequireVerification = prefs.getBool(_requireVerificationKey) ?? _defaultRequireVerification;
|
||||
return _cachedRequireVerification!;
|
||||
}
|
||||
|
||||
/// 设置是否需要验证
|
||||
static Future<void> setVerificationRequired(bool required) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_requireVerificationKey, required);
|
||||
_cachedRequireVerification = required;
|
||||
}
|
||||
|
||||
/// 获取可用的注册方式列表
|
||||
static Future<List<RegistrationMethod>> getAvailableRegistrationMethods() async {
|
||||
final List<RegistrationMethod> methods = [];
|
||||
|
||||
if (await isEmailRegistrationEnabled()) {
|
||||
methods.add(RegistrationMethod.email);
|
||||
}
|
||||
|
||||
if (await isPhoneRegistrationEnabled()) {
|
||||
methods.add(RegistrationMethod.phone);
|
||||
}
|
||||
|
||||
return methods;
|
||||
}
|
||||
|
||||
/// 检查是否至少有一种注册方式可用
|
||||
static Future<bool> hasAvailableRegistrationMethod() async {
|
||||
final methods = await getAvailableRegistrationMethods();
|
||||
return methods.isNotEmpty;
|
||||
}
|
||||
|
||||
/// 重置所有配置到默认值
|
||||
static Future<void> resetToDefaults() async {
|
||||
await setPhoneRegistrationEnabled(_defaultPhoneRegistrationEnabled);
|
||||
await setEmailRegistrationEnabled(_defaultEmailRegistrationEnabled);
|
||||
await setVerificationRequired(_defaultRequireVerification);
|
||||
await setQuickRegistrationEnabled(_defaultQuickRegistrationEnabled);
|
||||
}
|
||||
|
||||
/// 清除缓存
|
||||
static void clearCache() {
|
||||
_cachedPhoneRegistrationEnabled = null;
|
||||
_cachedEmailRegistrationEnabled = null;
|
||||
_cachedRequireVerification = null;
|
||||
_cachedQuickRegistrationEnabled = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册方式枚举
|
||||
enum RegistrationMethod {
|
||||
email('邮箱注册', 'email'),
|
||||
phone('手机注册', 'phone');
|
||||
|
||||
const RegistrationMethod(this.displayName, this.value);
|
||||
|
||||
final String displayName;
|
||||
final String value;
|
||||
}
|
||||
|
||||
/// 注册配置数据类
|
||||
class RegistrationConfig {
|
||||
const RegistrationConfig({
|
||||
required this.phoneRegistrationEnabled,
|
||||
required this.emailRegistrationEnabled,
|
||||
required this.verificationRequired,
|
||||
this.quickRegistrationEnabled = true,
|
||||
});
|
||||
|
||||
final bool phoneRegistrationEnabled;
|
||||
final bool emailRegistrationEnabled;
|
||||
final bool verificationRequired;
|
||||
final bool quickRegistrationEnabled;
|
||||
|
||||
/// 获取可用的注册方式
|
||||
List<RegistrationMethod> get availableMethods {
|
||||
final List<RegistrationMethod> methods = [];
|
||||
|
||||
if (emailRegistrationEnabled) {
|
||||
methods.add(RegistrationMethod.email);
|
||||
}
|
||||
|
||||
if (phoneRegistrationEnabled) {
|
||||
methods.add(RegistrationMethod.phone);
|
||||
}
|
||||
|
||||
return methods;
|
||||
}
|
||||
|
||||
/// 是否至少有一种注册方式可用
|
||||
bool get hasAvailableMethod => availableMethods.isNotEmpty;
|
||||
|
||||
/// 复制配置
|
||||
RegistrationConfig copyWith({
|
||||
bool? phoneRegistrationEnabled,
|
||||
bool? emailRegistrationEnabled,
|
||||
bool? verificationRequired,
|
||||
bool? quickRegistrationEnabled,
|
||||
}) {
|
||||
return RegistrationConfig(
|
||||
phoneRegistrationEnabled: phoneRegistrationEnabled ?? this.phoneRegistrationEnabled,
|
||||
emailRegistrationEnabled: emailRegistrationEnabled ?? this.emailRegistrationEnabled,
|
||||
verificationRequired: verificationRequired ?? this.verificationRequired,
|
||||
quickRegistrationEnabled: quickRegistrationEnabled ?? this.quickRegistrationEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is RegistrationConfig &&
|
||||
other.phoneRegistrationEnabled == phoneRegistrationEnabled &&
|
||||
other.emailRegistrationEnabled == emailRegistrationEnabled &&
|
||||
other.verificationRequired == verificationRequired &&
|
||||
other.quickRegistrationEnabled == quickRegistrationEnabled;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
phoneRegistrationEnabled,
|
||||
emailRegistrationEnabled,
|
||||
verificationRequired,
|
||||
quickRegistrationEnabled,
|
||||
);
|
||||
}
|
||||
89
AINoval/lib/models/chapters_for_preload_dto.dart
Normal file
89
AINoval/lib/models/chapters_for_preload_dto.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
import 'novel_structure.dart';
|
||||
|
||||
/// 预加载章节数据传输对象
|
||||
/// 专门用于阅读器预加载功能,包含章节列表和对应的场景内容
|
||||
class ChaptersForPreloadDto {
|
||||
const ChaptersForPreloadDto({
|
||||
required this.chapters,
|
||||
required this.scenesByChapter,
|
||||
});
|
||||
|
||||
/// 从JSON创建实例
|
||||
factory ChaptersForPreloadDto.fromJson(Map<String, dynamic> json) {
|
||||
// 解析章节列表
|
||||
final List<Chapter> chaptersList = [];
|
||||
if (json['chapters'] != null && json['chapters'] is List) {
|
||||
chaptersList.addAll(
|
||||
(json['chapters'] as List<dynamic>)
|
||||
.map((chapterJson) => Chapter.fromJson(chapterJson as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
// 解析按章节分组的场景
|
||||
final Map<String, List<Scene>> scenesMap = {};
|
||||
if (json['scenesByChapter'] != null && json['scenesByChapter'] is Map) {
|
||||
final rawScenesMap = json['scenesByChapter'] as Map<String, dynamic>;
|
||||
for (final entry in rawScenesMap.entries) {
|
||||
final chapterId = entry.key;
|
||||
final scenesList = <Scene>[];
|
||||
|
||||
if (entry.value is List) {
|
||||
scenesList.addAll(
|
||||
(entry.value as List<dynamic>)
|
||||
.map((sceneJson) => Scene.fromJson(sceneJson as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
scenesMap[chapterId] = scenesList;
|
||||
}
|
||||
}
|
||||
|
||||
return ChaptersForPreloadDto(
|
||||
chapters: chaptersList,
|
||||
scenesByChapter: scenesMap,
|
||||
);
|
||||
}
|
||||
|
||||
/// 章节列表,按顺序排列
|
||||
final List<Chapter> chapters;
|
||||
|
||||
/// 按章节ID分组的场景列表
|
||||
/// Key: 章节ID
|
||||
/// Value: 该章节的场景列表(按sequence排序)
|
||||
final Map<String, List<Scene>> scenesByChapter;
|
||||
|
||||
/// 获取章节总数
|
||||
int get chapterCount => chapters.length;
|
||||
|
||||
/// 获取场景总数
|
||||
int get totalSceneCount {
|
||||
return scenesByChapter.values
|
||||
.map((scenes) => scenes.length)
|
||||
.fold(0, (sum, count) => sum + count);
|
||||
}
|
||||
|
||||
/// 检查是否包含指定章节的数据
|
||||
bool containsChapter(String chapterId) {
|
||||
return chapters.any((chapter) => chapter.id == chapterId);
|
||||
}
|
||||
|
||||
/// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'chapters': chapters.map((chapter) => chapter.toJson()).toList(),
|
||||
'scenesByChapter': scenesByChapter.map(
|
||||
(chapterId, scenes) => MapEntry(
|
||||
chapterId,
|
||||
scenes.map((scene) => scene.toJson()).toList(),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ChaptersForPreloadDto(chapterCount: $chapterCount, totalSceneCount: $totalSceneCount)';
|
||||
}
|
||||
}
|
||||
156
AINoval/lib/models/chat_message.dart
Normal file
156
AINoval/lib/models/chat_message.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
/// 消息发送者枚举
|
||||
enum MessageSender {
|
||||
user, // 用户发送的消息
|
||||
ai, // AI助手发送的消息
|
||||
}
|
||||
|
||||
// 可以为消息状态定义一个枚举
|
||||
enum MessageStatus {
|
||||
sending,
|
||||
sent,
|
||||
delivered,
|
||||
read,
|
||||
error,
|
||||
unknown, // 处理未知状态
|
||||
}
|
||||
|
||||
// 可以为消息类型定义一个枚举
|
||||
enum MessageType {
|
||||
text,
|
||||
image,
|
||||
audio,
|
||||
command,
|
||||
unknown, // 处理未知类型
|
||||
}
|
||||
|
||||
/// 聊天消息模型
|
||||
class ChatMessage {
|
||||
|
||||
/// 构造函数
|
||||
ChatMessage({
|
||||
required this.id,
|
||||
required this.content,
|
||||
required this.sender,
|
||||
required this.timestamp,
|
||||
// 添加新字段,设为可选,以便旧数据或不需要这些字段的地方能兼容
|
||||
this.sessionId,
|
||||
this.status,
|
||||
this.messageType,
|
||||
this.metadata,
|
||||
});
|
||||
|
||||
/// 从JSON创建ChatMessage实例
|
||||
factory ChatMessage.fromJson(Map<String, dynamic> json) {
|
||||
// 修正:根据后端 'role' 字段映射到 'sender' 枚举
|
||||
MessageSender sender;
|
||||
final role = json['role'] as String?;
|
||||
if (role == 'assistant') {
|
||||
sender = MessageSender.ai;
|
||||
} else if (role == 'user') {
|
||||
sender = MessageSender.user;
|
||||
} else {
|
||||
sender = MessageSender.ai; // 或其他默认处理
|
||||
print("Warning: Unknown message role '$role' received, mapping to 'ai'.");
|
||||
}
|
||||
|
||||
// 解析 status (可选)
|
||||
MessageStatus? status;
|
||||
final statusString = json['status'] as String?;
|
||||
if (statusString != null) {
|
||||
try {
|
||||
status = MessageStatus.values.byName(statusString.toLowerCase());
|
||||
} catch (e) {
|
||||
status = MessageStatus.unknown;
|
||||
print("Warning: Unknown message status '$statusString' received.");
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 messageType (可选)
|
||||
MessageType? messageType;
|
||||
final typeString = json['messageType'] as String?;
|
||||
if (typeString != null) {
|
||||
try {
|
||||
messageType = MessageType.values.byName(typeString.toLowerCase());
|
||||
} catch (e) {
|
||||
messageType = MessageType.unknown;
|
||||
print("Warning: Unknown message type '$typeString' received.");
|
||||
}
|
||||
}
|
||||
|
||||
return ChatMessage(
|
||||
id: json['id'] as String,
|
||||
content: json['content'] as String,
|
||||
sender: sender, // 使用上面转换后的 sender
|
||||
// 修正:读取 'createdAt' 字段并解析为 DateTime
|
||||
timestamp: DateTime.parse(json['createdAt'] as String),
|
||||
// 读取新添加的可选字段
|
||||
sessionId: json['sessionId'] as String?,
|
||||
status: status,
|
||||
messageType: messageType,
|
||||
metadata: json['metadata'] as Map<String, dynamic>?, // Dart 中通常用 Map<String, dynamic>
|
||||
);
|
||||
}
|
||||
/// 消息唯一标识符
|
||||
final String id;
|
||||
|
||||
/// 消息内容
|
||||
final String content;
|
||||
|
||||
/// 消息发送者
|
||||
final MessageSender sender;
|
||||
|
||||
/// 消息发送时间
|
||||
final DateTime timestamp;
|
||||
|
||||
// --- 新添加的字段 ---
|
||||
/// 会话ID (可选)
|
||||
final String? sessionId;
|
||||
|
||||
/// 消息状态 (可选)
|
||||
final MessageStatus? status;
|
||||
|
||||
/// 消息类型 (可选)
|
||||
final MessageType? messageType;
|
||||
|
||||
/// 消息元数据 (可选)
|
||||
final Map<String, dynamic>? metadata;
|
||||
// --- 结束 ---
|
||||
|
||||
|
||||
/// 将ChatMessage实例转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
// 修正:将 sender 枚举映射到后端的 'role' 字符串
|
||||
String role;
|
||||
if (sender == MessageSender.ai) {
|
||||
role = 'assistant';
|
||||
} else {
|
||||
role = 'user';
|
||||
}
|
||||
|
||||
// 注意:通常前端发送消息时,不需要发送所有字段给后端
|
||||
// 比如 status, messageType 可能由后端确定或不需要前端发送
|
||||
// sessionId 通常在请求的 URL 或其他地方指定,而不是在消息体里
|
||||
// metadata 可能需要发送
|
||||
// 这里我们只包含基础字段和 metadata 示例,根据你的 API 设计调整
|
||||
final data = <String, dynamic>{
|
||||
'id': id, // id 通常由后端生成,发送时可能不需要或为空
|
||||
'content': content,
|
||||
// 修正:使用 'role' 键和映射后的值
|
||||
'role': role,
|
||||
// 修正:使用 'createdAt' 键 (或者后端会自己设置时间戳?根据API定)
|
||||
// 'createdAt': timestamp.toIso8601String(), // 如果需要前端指定创建时间
|
||||
};
|
||||
|
||||
// 按需添加其他字段到发送的 JSON 中
|
||||
if (sessionId != null) {
|
||||
// 通常 sessionId 不在消息体里发送,而是在 URL 或 DTO 的顶层字段
|
||||
// data['sessionId'] = sessionId;
|
||||
}
|
||||
if (metadata != null) {
|
||||
data['metadata'] = metadata;
|
||||
}
|
||||
// status 和 messageType 通常不由前端指定发送
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
456
AINoval/lib/models/chat_models.dart
Normal file
456
AINoval/lib/models/chat_models.dart
Normal file
@@ -0,0 +1,456 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import '../utils/date_time_parser.dart';
|
||||
|
||||
// 聊天会话模型
|
||||
class ChatSession {
|
||||
ChatSession({
|
||||
required this.id,
|
||||
required this.title,
|
||||
String? selectedModelConfigId,
|
||||
required this.createdAt,
|
||||
required this.lastUpdatedAt,
|
||||
required this.novelId,
|
||||
this.chapterId,
|
||||
this.status,
|
||||
this.messageCount,
|
||||
this.metadata,
|
||||
}) : selectedModelConfigId = selectedModelConfigId;
|
||||
|
||||
// 从JSON转换方法
|
||||
factory ChatSession.fromJson(Map<String, dynamic> json) {
|
||||
// 辅助函数安全地获取和转换 String
|
||||
String safeString(String key, [String defaultValue = '']) {
|
||||
return json[key] as String? ?? defaultValue;
|
||||
}
|
||||
|
||||
// 辅助函数安全地获取和解析 DateTime
|
||||
DateTime safeDateTime(String key, DateTime defaultValue) {
|
||||
final value = json[key] as String?;
|
||||
return value != null
|
||||
? (DateTime.tryParse(value) ?? defaultValue)
|
||||
: defaultValue;
|
||||
}
|
||||
|
||||
return ChatSession(
|
||||
// 使用 sessionId 作为 id,并提供一个默认空字符串以防万一
|
||||
id: safeString('sessionId'),
|
||||
title: safeString('title', '无标题会话'),
|
||||
selectedModelConfigId: json['selectedModelConfigId'] as String?,
|
||||
createdAt: parseBackendDateTime(json['createdAt']),
|
||||
lastUpdatedAt: parseBackendDateTime(json['updatedAt']),
|
||||
novelId: safeString('novelId'),
|
||||
chapterId: json['chapterId'] as String?,
|
||||
status: json['status'] as String?,
|
||||
messageCount: (json['messageCount'] as num?)?.toInt() ?? 0,
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
final String id;
|
||||
final String title;
|
||||
final String? selectedModelConfigId;
|
||||
final DateTime createdAt;
|
||||
final DateTime lastUpdatedAt;
|
||||
final String novelId;
|
||||
final String? chapterId;
|
||||
final String? status;
|
||||
final int? messageCount;
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
// 复制方法,用于创建会话的副本
|
||||
ChatSession copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? selectedModelConfigId,
|
||||
DateTime? createdAt,
|
||||
DateTime? lastUpdatedAt,
|
||||
String? novelId,
|
||||
String? chapterId,
|
||||
String? status,
|
||||
int? messageCount,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) {
|
||||
return ChatSession(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
selectedModelConfigId:
|
||||
selectedModelConfigId ?? this.selectedModelConfigId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
lastUpdatedAt: lastUpdatedAt ?? this.lastUpdatedAt,
|
||||
novelId: novelId ?? this.novelId,
|
||||
chapterId: chapterId ?? this.chapterId,
|
||||
status: status ?? this.status,
|
||||
messageCount: messageCount ?? this.messageCount,
|
||||
metadata: metadata ?? this.metadata,
|
||||
);
|
||||
}
|
||||
|
||||
// 转换为JSON方法
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'selectedModelConfigId': selectedModelConfigId,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'lastUpdatedAt': lastUpdatedAt.toIso8601String(),
|
||||
'novelId': novelId,
|
||||
'chapterId': chapterId,
|
||||
'status': status,
|
||||
'messageCount': messageCount,
|
||||
'metadata': metadata,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is ChatSession &&
|
||||
other.id == id &&
|
||||
other.title == title &&
|
||||
other.selectedModelConfigId == selectedModelConfigId &&
|
||||
other.createdAt == createdAt &&
|
||||
other.lastUpdatedAt == lastUpdatedAt &&
|
||||
other.novelId == novelId &&
|
||||
other.chapterId == chapterId &&
|
||||
other.status == status &&
|
||||
other.messageCount == messageCount &&
|
||||
other.metadata == metadata;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
title.hashCode ^
|
||||
selectedModelConfigId.hashCode ^
|
||||
createdAt.hashCode ^
|
||||
lastUpdatedAt.hashCode ^
|
||||
novelId.hashCode ^
|
||||
chapterId.hashCode ^
|
||||
status.hashCode ^
|
||||
messageCount.hashCode ^
|
||||
metadata.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
// 聊天消息模型
|
||||
class ChatMessage {
|
||||
ChatMessage({
|
||||
required this.id,
|
||||
required this.role,
|
||||
required this.content,
|
||||
required this.timestamp,
|
||||
this.status = MessageStatus.sent,
|
||||
this.actions,
|
||||
this.sessionId,
|
||||
this.userId,
|
||||
this.novelId,
|
||||
this.modelName,
|
||||
this.metadata,
|
||||
required this.sender,
|
||||
});
|
||||
|
||||
// 从JSON转换方法
|
||||
factory ChatMessage.fromJson(Map<String, dynamic> json) {
|
||||
// --- Helper for safe string parsing ---
|
||||
String safeString(String key, [String defaultValue = '']) {
|
||||
final value = json[key];
|
||||
if (value is String) return value;
|
||||
// Log or handle non-string/null cases if needed
|
||||
// AppLogger.w('ChatMessage.fromJson', 'Expected String for key "$key", but got ${value?.runtimeType}. Using default.');
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
List<MessageAction>? parsedActions;
|
||||
if (json['metadata'] != null && json['metadata']['actions'] is List) {
|
||||
parsedActions = (json['metadata']['actions'] as List)
|
||||
.map((e) => MessageAction.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// --- Handle potentially null 'id' safely ---
|
||||
// Provide a temporary unique default if 'id' is null.
|
||||
// The Bloc logic will prioritize its own placeholder ID anyway.
|
||||
final messageId = safeString('id', 'temp_chunk_${const Uuid().v4()}');
|
||||
|
||||
return ChatMessage(
|
||||
id: messageId, // Use the safe ID
|
||||
role: MessageRole.values.firstWhere(
|
||||
// Use safeString for role
|
||||
(e) =>
|
||||
e.name == safeString('role', MessageRole.system.name).toLowerCase(),
|
||||
orElse: () => MessageRole.system, // Fallback
|
||||
),
|
||||
// Use safeString for content, important for potentially empty chunks
|
||||
content: safeString('content'),
|
||||
// Assume parseBackendDateTime handles the list format or null
|
||||
// Provide a fallback default DateTime if key is missing or parsing fails
|
||||
timestamp: parseBackendDateTime(json['createdAt'] ?? DateTime.now()),
|
||||
status: MessageStatus.values.firstWhere(
|
||||
// Use safeString for status
|
||||
(e) =>
|
||||
e.name ==
|
||||
safeString('status', MessageStatus.sent.name).toLowerCase(),
|
||||
orElse: () => MessageStatus.sent, // Fallback
|
||||
),
|
||||
actions: parsedActions,
|
||||
// These fields allow null, direct access is relatively safe but casting is good practice
|
||||
sessionId: json['sessionId'] as String?,
|
||||
userId: json['userId'] as String?,
|
||||
novelId: json['novelId'] as String?,
|
||||
modelName: json['modelName'] as String?,
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
sender: MessageSender.values.firstWhere(
|
||||
(e) =>
|
||||
e.name ==
|
||||
safeString('sender', MessageSender.user.name).toLowerCase(),
|
||||
orElse: () => MessageSender.user,
|
||||
),
|
||||
);
|
||||
}
|
||||
final String id;
|
||||
final MessageRole role;
|
||||
final String content;
|
||||
final DateTime timestamp;
|
||||
final MessageStatus status;
|
||||
final List<MessageAction>? actions;
|
||||
final String? sessionId;
|
||||
final String? userId;
|
||||
final String? novelId;
|
||||
final String? modelName;
|
||||
final Map<String, dynamic>? metadata;
|
||||
final MessageSender sender;
|
||||
|
||||
// 复制方法
|
||||
ChatMessage copyWith({
|
||||
String? id,
|
||||
MessageRole? role,
|
||||
String? content,
|
||||
DateTime? timestamp,
|
||||
MessageStatus? status,
|
||||
List<MessageAction>? actions,
|
||||
String? sessionId,
|
||||
String? userId,
|
||||
String? novelId,
|
||||
String? modelName,
|
||||
Map<String, dynamic>? metadata,
|
||||
MessageSender? sender,
|
||||
}) {
|
||||
return ChatMessage(
|
||||
id: id ?? this.id,
|
||||
role: role ?? this.role,
|
||||
content: content ?? this.content,
|
||||
timestamp: timestamp ?? this.timestamp,
|
||||
status: status ?? this.status,
|
||||
actions: actions ?? this.actions,
|
||||
sessionId: sessionId ?? this.sessionId,
|
||||
userId: userId ?? this.userId,
|
||||
novelId: novelId ?? this.novelId,
|
||||
modelName: modelName ?? this.modelName,
|
||||
metadata: metadata ?? this.metadata,
|
||||
sender: sender ?? this.sender,
|
||||
);
|
||||
}
|
||||
|
||||
// 转换为JSON方法
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> currentMetadata = Map.from(metadata ?? {});
|
||||
if (actions != null) {
|
||||
currentMetadata['actions'] = actions!.map((e) => e.toJson()).toList();
|
||||
}
|
||||
|
||||
return {
|
||||
'id': id,
|
||||
'role': role.name,
|
||||
'content': content,
|
||||
'createdAt': timestamp.toIso8601String(),
|
||||
'status': status.name,
|
||||
'sessionId': sessionId,
|
||||
'userId': userId,
|
||||
'novelId': novelId,
|
||||
'modelName': modelName,
|
||||
'metadata': currentMetadata.isEmpty ? null : currentMetadata,
|
||||
'sender': sender.name,
|
||||
};
|
||||
}
|
||||
|
||||
// 格式化时间戳
|
||||
String get formattedTime => DateFormat('HH:mm').format(timestamp);
|
||||
|
||||
// 格式化日期
|
||||
String get formattedDate => DateFormat('yyyy-MM-dd').format(timestamp);
|
||||
}
|
||||
|
||||
// 消息发送者角色
|
||||
enum MessageRole {
|
||||
user,
|
||||
assistant,
|
||||
system,
|
||||
}
|
||||
|
||||
// 消息状态
|
||||
enum MessageStatus {
|
||||
sending,
|
||||
sent,
|
||||
error,
|
||||
pending,
|
||||
delivered,
|
||||
read,
|
||||
streaming,
|
||||
}
|
||||
|
||||
// 消息关联操作
|
||||
class MessageAction {
|
||||
MessageAction({
|
||||
required this.id,
|
||||
required this.label,
|
||||
required this.type,
|
||||
this.data,
|
||||
});
|
||||
|
||||
// 从JSON转换方法
|
||||
factory MessageAction.fromJson(Map<String, dynamic> json) {
|
||||
return MessageAction(
|
||||
id: json['id'] as String,
|
||||
label: json['label'] as String,
|
||||
type: ActionType.values.firstWhere(
|
||||
(e) => e.toString() == 'ActionType.${json['type']}',
|
||||
),
|
||||
data: json['data'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
final String id;
|
||||
final String label;
|
||||
final ActionType type;
|
||||
final Map<String, dynamic>? data;
|
||||
|
||||
// 转换为JSON方法
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'label': label,
|
||||
'type': type.toString().split('.').last,
|
||||
'data': data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 操作类型
|
||||
enum ActionType {
|
||||
applyToEditor,
|
||||
createCharacter,
|
||||
createLocation,
|
||||
generatePlot,
|
||||
expandScene,
|
||||
createChapter,
|
||||
analyzeSentiment,
|
||||
fixGrammar,
|
||||
}
|
||||
|
||||
// 聊天上下文模型
|
||||
class ChatContext {
|
||||
ChatContext({
|
||||
required this.novelId,
|
||||
this.chapterId,
|
||||
this.selectedText,
|
||||
this.relevantItems = const [],
|
||||
});
|
||||
|
||||
// 从JSON转换方法
|
||||
factory ChatContext.fromJson(Map<String, dynamic> json) {
|
||||
return ChatContext(
|
||||
novelId: json['novelId'] as String,
|
||||
chapterId: json['chapterId'] as String?,
|
||||
selectedText: json['selectedText'] as String?,
|
||||
relevantItems: json['relevantItems'] != null
|
||||
? (json['relevantItems'] as List)
|
||||
.map((e) => ContextItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: [],
|
||||
);
|
||||
}
|
||||
final String novelId;
|
||||
final String? chapterId;
|
||||
final String? selectedText;
|
||||
final List<ContextItem> relevantItems;
|
||||
|
||||
// 复制方法
|
||||
ChatContext copyWith({
|
||||
String? novelId,
|
||||
String? chapterId,
|
||||
String? selectedText,
|
||||
List<ContextItem>? relevantItems,
|
||||
}) {
|
||||
return ChatContext(
|
||||
novelId: novelId ?? this.novelId,
|
||||
chapterId: chapterId ?? this.chapterId,
|
||||
selectedText: selectedText ?? this.selectedText,
|
||||
relevantItems: relevantItems ?? this.relevantItems,
|
||||
);
|
||||
}
|
||||
|
||||
// 转换为JSON方法
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'novelId': novelId,
|
||||
'chapterId': chapterId,
|
||||
'selectedText': selectedText,
|
||||
'relevantItems': relevantItems.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 上下文项目
|
||||
class ContextItem {
|
||||
ContextItem({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.title,
|
||||
required this.content,
|
||||
required this.relevanceScore,
|
||||
});
|
||||
|
||||
// 从JSON转换方法
|
||||
factory ContextItem.fromJson(Map<String, dynamic> json) {
|
||||
return ContextItem(
|
||||
id: json['id'] as String,
|
||||
type: ContextItemType.values.firstWhere(
|
||||
(e) => e.toString() == 'ContextItemType.${json['type']}',
|
||||
),
|
||||
title: json['title'] as String,
|
||||
content: json['content'] as String,
|
||||
relevanceScore: json['relevanceScore'] as double,
|
||||
);
|
||||
}
|
||||
final String id;
|
||||
final ContextItemType type;
|
||||
final String title;
|
||||
final String content;
|
||||
final double relevanceScore;
|
||||
|
||||
// 转换为JSON方法
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'type': type.toString().split('.').last,
|
||||
'title': title,
|
||||
'content': content,
|
||||
'relevanceScore': relevanceScore,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 上下文项目类型
|
||||
enum ContextItemType {
|
||||
character,
|
||||
location,
|
||||
plot,
|
||||
chapter,
|
||||
scene,
|
||||
note,
|
||||
lore,
|
||||
}
|
||||
|
||||
// 消息发送者
|
||||
enum MessageSender { user, ai }
|
||||
41
AINoval/lib/models/compose_preview.dart
Normal file
41
AINoval/lib/models/compose_preview.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
class ComposeChapterPreview {
|
||||
final int index;
|
||||
final String title;
|
||||
final String outline;
|
||||
final String content;
|
||||
|
||||
const ComposeChapterPreview({
|
||||
required this.index,
|
||||
this.title = '',
|
||||
this.outline = '',
|
||||
this.content = '',
|
||||
});
|
||||
|
||||
ComposeChapterPreview copyWith({
|
||||
String? title,
|
||||
String? outline,
|
||||
String? content,
|
||||
}) {
|
||||
return ComposeChapterPreview(
|
||||
index: index,
|
||||
title: title ?? this.title,
|
||||
outline: outline ?? this.outline,
|
||||
content: content ?? this.content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ComposeReadyInfo {
|
||||
final bool ready;
|
||||
final String reason;
|
||||
final String novelId;
|
||||
final String sessionId;
|
||||
const ComposeReadyInfo({
|
||||
required this.ready,
|
||||
required this.reason,
|
||||
required this.novelId,
|
||||
required this.sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
1193
AINoval/lib/models/context_selection_models.dart
Normal file
1193
AINoval/lib/models/context_selection_models.dart
Normal file
File diff suppressed because it is too large
Load Diff
252
AINoval/lib/models/dto/novel_setting_dto.dart
Normal file
252
AINoval/lib/models/dto/novel_setting_dto.dart
Normal file
@@ -0,0 +1,252 @@
|
||||
/// 设定条目列表请求DTO
|
||||
class SettingItemListRequest {
|
||||
final String? type;
|
||||
final String? name;
|
||||
final int? priority;
|
||||
final String? generatedBy;
|
||||
final String? status;
|
||||
final int page;
|
||||
final int size;
|
||||
final String sortBy;
|
||||
final String sortDirection;
|
||||
|
||||
SettingItemListRequest({
|
||||
this.type,
|
||||
this.name,
|
||||
this.priority,
|
||||
this.generatedBy,
|
||||
this.status,
|
||||
this.page = 0,
|
||||
this.size = 20,
|
||||
this.sortBy = 'createdAt',
|
||||
this.sortDirection = 'desc',
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
if (type != null) data['type'] = type;
|
||||
if (name != null) data['name'] = name;
|
||||
if (priority != null) data['priority'] = priority;
|
||||
if (generatedBy != null) data['generatedBy'] = generatedBy;
|
||||
if (status != null) data['status'] = status;
|
||||
data['page'] = page;
|
||||
data['size'] = size;
|
||||
data['sortBy'] = sortBy;
|
||||
data['sortDirection'] = sortDirection;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/// 设定条目详情请求DTO
|
||||
class SettingItemDetailRequest {
|
||||
final String itemId;
|
||||
|
||||
SettingItemDetailRequest({required this.itemId});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'itemId': itemId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 设定条目更新请求DTO
|
||||
class SettingItemUpdateRequest {
|
||||
final String itemId;
|
||||
final dynamic settingItem;
|
||||
|
||||
SettingItemUpdateRequest({required this.itemId, required this.settingItem});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'itemId': itemId,
|
||||
'settingItem': settingItem,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 设定条目删除请求DTO
|
||||
class SettingItemDeleteRequest {
|
||||
final String itemId;
|
||||
|
||||
SettingItemDeleteRequest({required this.itemId});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'itemId': itemId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 设定关系请求DTO
|
||||
class SettingRelationshipRequest {
|
||||
final String itemId;
|
||||
final String targetItemId;
|
||||
final String relationshipType;
|
||||
final String? description;
|
||||
|
||||
SettingRelationshipRequest({
|
||||
required this.itemId,
|
||||
required this.targetItemId,
|
||||
required this.relationshipType,
|
||||
this.description,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['itemId'] = itemId;
|
||||
data['targetItemId'] = targetItemId;
|
||||
data['relationshipType'] = relationshipType;
|
||||
if (description != null) data['description'] = description;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/// 设定关系删除请求DTO
|
||||
class SettingRelationshipDeleteRequest {
|
||||
final String itemId;
|
||||
final String targetItemId;
|
||||
final String relationshipType;
|
||||
|
||||
SettingRelationshipDeleteRequest({
|
||||
required this.itemId,
|
||||
required this.targetItemId,
|
||||
required this.relationshipType,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'itemId': itemId,
|
||||
'targetItemId': targetItemId,
|
||||
'relationshipType': relationshipType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 设定组列表请求DTO
|
||||
class SettingGroupListRequest {
|
||||
final String? name;
|
||||
final bool? isActiveContext;
|
||||
|
||||
SettingGroupListRequest({this.name, this.isActiveContext});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
if (name != null) data['name'] = name;
|
||||
if (isActiveContext != null) data['isActiveContext'] = isActiveContext;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/// 设定组详情请求DTO
|
||||
class SettingGroupDetailRequest {
|
||||
final String groupId;
|
||||
|
||||
SettingGroupDetailRequest({required this.groupId});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'groupId': groupId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 设定组更新请求DTO
|
||||
class SettingGroupUpdateRequest {
|
||||
final String groupId;
|
||||
final dynamic settingGroup;
|
||||
|
||||
SettingGroupUpdateRequest({required this.groupId, required this.settingGroup});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'groupId': groupId,
|
||||
'settingGroup': settingGroup,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 设定组删除请求DTO
|
||||
class SettingGroupDeleteRequest {
|
||||
final String groupId;
|
||||
|
||||
SettingGroupDeleteRequest({required this.groupId});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'groupId': groupId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 设定组条目请求DTO
|
||||
class GroupItemRequest {
|
||||
final String groupId;
|
||||
final String itemId;
|
||||
|
||||
GroupItemRequest({required this.groupId, required this.itemId});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'groupId': groupId,
|
||||
'itemId': itemId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置设定组激活状态请求DTO
|
||||
class SetGroupActiveRequest {
|
||||
final String groupId;
|
||||
final bool active;
|
||||
|
||||
SetGroupActiveRequest({required this.groupId, required this.active});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'groupId': groupId,
|
||||
'active': active,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 从文本提取设定条目请求DTO
|
||||
class ExtractSettingsRequest {
|
||||
final String text;
|
||||
final String type;
|
||||
|
||||
ExtractSettingsRequest({required this.text, required this.type});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'text': text,
|
||||
'type': type,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 搜索设定条目请求DTO
|
||||
class SettingSearchRequest {
|
||||
final String query;
|
||||
final List<String>? types;
|
||||
final List<String>? groupIds;
|
||||
final double? minScore;
|
||||
final int? maxResults;
|
||||
|
||||
SettingSearchRequest({
|
||||
required this.query,
|
||||
this.types,
|
||||
this.groupIds,
|
||||
this.minScore,
|
||||
this.maxResults,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['query'] = query;
|
||||
if (types != null) data['types'] = types;
|
||||
if (groupIds != null) data['groupIds'] = groupIds;
|
||||
if (minScore != null) data['minScore'] = minScore;
|
||||
if (maxResults != null) data['maxResults'] = maxResults;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
170
AINoval/lib/models/editor_content.dart
Normal file
170
AINoval/lib/models/editor_content.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class EditorContent extends Equatable {
|
||||
|
||||
const EditorContent({
|
||||
required this.id,
|
||||
required this.content,
|
||||
required this.lastSaved,
|
||||
this.revisions = const [],
|
||||
this.scenes,
|
||||
});
|
||||
|
||||
// 从JSON转换
|
||||
factory EditorContent.fromJson(Map<String, dynamic> json) {
|
||||
Map<String, SceneContent>? scenesMap;
|
||||
if (json['scenes'] != null) {
|
||||
scenesMap = {};
|
||||
json['scenes'].forEach((key, value) {
|
||||
scenesMap![key] = SceneContent.fromJson(value);
|
||||
});
|
||||
}
|
||||
|
||||
return EditorContent(
|
||||
id: json['id'],
|
||||
content: json['content'],
|
||||
lastSaved: DateTime.parse(json['lastSaved']),
|
||||
revisions: (json['revisions'] as List?)
|
||||
?.map((e) => Revision.fromJson(e))
|
||||
.toList() ?? [],
|
||||
scenes: scenesMap,
|
||||
);
|
||||
}
|
||||
final String id;
|
||||
final String content;
|
||||
final DateTime lastSaved;
|
||||
final List<Revision> revisions;
|
||||
final Map<String, SceneContent>? scenes;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, content, lastSaved, revisions, scenes];
|
||||
|
||||
// 创建副本但更新部分内容
|
||||
EditorContent copyWith({
|
||||
String? id,
|
||||
String? content,
|
||||
DateTime? lastSaved,
|
||||
List<Revision>? revisions,
|
||||
Map<String, SceneContent>? scenes,
|
||||
}) {
|
||||
return EditorContent(
|
||||
id: id ?? this.id,
|
||||
content: content ?? this.content,
|
||||
lastSaved: lastSaved ?? this.lastSaved,
|
||||
revisions: revisions ?? this.revisions,
|
||||
scenes: scenes ?? this.scenes,
|
||||
);
|
||||
}
|
||||
|
||||
// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = {
|
||||
'id': id,
|
||||
'content': content,
|
||||
'lastSaved': lastSaved.toIso8601String(),
|
||||
'revisions': revisions.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
if (scenes != null) {
|
||||
data['scenes'] = {};
|
||||
scenes!.forEach((key, value) {
|
||||
data['scenes'][key] = value.toJson();
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class Revision extends Equatable {
|
||||
|
||||
const Revision({
|
||||
required this.id,
|
||||
required this.content,
|
||||
required this.timestamp,
|
||||
required this.authorId,
|
||||
this.comment = '',
|
||||
});
|
||||
|
||||
// 从JSON转换
|
||||
factory Revision.fromJson(Map<String, dynamic> json) {
|
||||
return Revision(
|
||||
id: json['id'],
|
||||
content: json['content'],
|
||||
timestamp: DateTime.parse(json['timestamp']),
|
||||
authorId: json['authorId'],
|
||||
comment: json['comment'] ?? '',
|
||||
);
|
||||
}
|
||||
final String id;
|
||||
final String content;
|
||||
final DateTime timestamp;
|
||||
final String authorId;
|
||||
final String comment;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, content, timestamp, authorId, comment];
|
||||
|
||||
// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'content': content,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'authorId': authorId,
|
||||
'comment': comment,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class SceneContent extends Equatable {
|
||||
|
||||
const SceneContent({
|
||||
required this.content,
|
||||
required this.summary,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
});
|
||||
|
||||
// 从JSON转换
|
||||
factory SceneContent.fromJson(Map<String, dynamic> json) {
|
||||
return SceneContent(
|
||||
content: json['content'] ?? '',
|
||||
summary: json['summary'] ?? '',
|
||||
title: json['title'] ?? '',
|
||||
subtitle: json['subtitle'] ?? '',
|
||||
);
|
||||
}
|
||||
final String content;
|
||||
final String summary;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [content, summary, title, subtitle];
|
||||
|
||||
// 创建副本但更新部分内容
|
||||
SceneContent copyWith({
|
||||
String? content,
|
||||
String? summary,
|
||||
String? title,
|
||||
String? subtitle,
|
||||
}) {
|
||||
return SceneContent(
|
||||
content: content ?? this.content,
|
||||
summary: summary ?? this.summary,
|
||||
title: title ?? this.title,
|
||||
subtitle: subtitle ?? this.subtitle,
|
||||
);
|
||||
}
|
||||
|
||||
// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'content': content,
|
||||
'summary': summary,
|
||||
'title': title,
|
||||
'subtitle': subtitle,
|
||||
};
|
||||
}
|
||||
}
|
||||
371
AINoval/lib/models/editor_settings.dart
Normal file
371
AINoval/lib/models/editor_settings.dart
Normal file
@@ -0,0 +1,371 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 编辑器设置模型
|
||||
/// 包含编辑器的所有可定制化选项
|
||||
class EditorSettings {
|
||||
const EditorSettings({
|
||||
// 字体相关设置
|
||||
this.fontSize = 16.0,
|
||||
this.fontFamily = 'serif', // 🚀 改为中文友好的默认字体
|
||||
this.fontWeight = FontWeight.normal,
|
||||
this.lineSpacing = 1.5,
|
||||
this.letterSpacing = 0.0, // 🚀 中文写作建议稍微调整字符间距
|
||||
|
||||
// 间距和布局设置
|
||||
this.paddingHorizontal = 16.0,
|
||||
this.paddingVertical = 12.0,
|
||||
this.paragraphSpacing = 8.0,
|
||||
this.indentSize = 32.0,
|
||||
|
||||
// 编辑器行为设置
|
||||
this.autoSaveEnabled = true,
|
||||
this.autoSaveIntervalMinutes = 5,
|
||||
this.spellCheckEnabled = true,
|
||||
this.showWordCount = true,
|
||||
this.showLineNumbers = false,
|
||||
this.highlightActiveLine = true,
|
||||
|
||||
// 主题和外观设置
|
||||
this.darkModeEnabled = false,
|
||||
this.showMiniMap = false,
|
||||
this.smoothScrolling = true,
|
||||
this.fadeInAnimation = true,
|
||||
|
||||
// 主题变体
|
||||
this.themeVariant = 'monochrome',
|
||||
|
||||
// 编辑器宽度和高度设置
|
||||
this.maxLineWidth = 1500.0,
|
||||
this.minEditorHeight = 1200.0,
|
||||
this.useTypewriterMode = false,
|
||||
|
||||
// 文本选择和光标设置
|
||||
this.cursorBlinkRate = 1.0,
|
||||
this.selectionHighlightColor = 0xFF2196F3,
|
||||
this.enableVimMode = false,
|
||||
|
||||
// 导出和打印设置
|
||||
this.defaultExportFormat = 'markdown',
|
||||
this.includeMetadata = true,
|
||||
});
|
||||
|
||||
// 字体相关设置
|
||||
final double fontSize;
|
||||
final String fontFamily;
|
||||
final FontWeight fontWeight;
|
||||
final double lineSpacing;
|
||||
final double letterSpacing;
|
||||
|
||||
// 间距和布局设置
|
||||
final double paddingHorizontal;
|
||||
final double paddingVertical;
|
||||
final double paragraphSpacing;
|
||||
final double indentSize;
|
||||
|
||||
// 编辑器行为设置
|
||||
final bool autoSaveEnabled;
|
||||
final int autoSaveIntervalMinutes;
|
||||
final bool spellCheckEnabled;
|
||||
final bool showWordCount;
|
||||
final bool showLineNumbers;
|
||||
final bool highlightActiveLine;
|
||||
|
||||
// 主题和外观设置
|
||||
final bool darkModeEnabled;
|
||||
final bool showMiniMap;
|
||||
final bool smoothScrolling;
|
||||
final bool fadeInAnimation;
|
||||
// 主题变体
|
||||
final String themeVariant;
|
||||
|
||||
// 编辑器宽度和高度设置
|
||||
final double maxLineWidth;
|
||||
final double minEditorHeight;
|
||||
final bool useTypewriterMode;
|
||||
|
||||
// 文本选择和光标设置
|
||||
final double cursorBlinkRate;
|
||||
final int selectionHighlightColor;
|
||||
final bool enableVimMode;
|
||||
|
||||
// 导出和打印设置
|
||||
final String defaultExportFormat;
|
||||
final bool includeMetadata;
|
||||
|
||||
/// 复制并修改设置
|
||||
EditorSettings copyWith({
|
||||
double? fontSize,
|
||||
String? fontFamily,
|
||||
FontWeight? fontWeight,
|
||||
double? lineSpacing,
|
||||
double? letterSpacing,
|
||||
double? paddingHorizontal,
|
||||
double? paddingVertical,
|
||||
double? paragraphSpacing,
|
||||
double? indentSize,
|
||||
bool? autoSaveEnabled,
|
||||
int? autoSaveIntervalMinutes,
|
||||
bool? spellCheckEnabled,
|
||||
bool? showWordCount,
|
||||
bool? showLineNumbers,
|
||||
bool? highlightActiveLine,
|
||||
bool? darkModeEnabled,
|
||||
bool? showMiniMap,
|
||||
bool? smoothScrolling,
|
||||
bool? fadeInAnimation,
|
||||
String? themeVariant,
|
||||
double? maxLineWidth,
|
||||
double? minEditorHeight,
|
||||
bool? useTypewriterMode,
|
||||
double? cursorBlinkRate,
|
||||
int? selectionHighlightColor,
|
||||
bool? enableVimMode,
|
||||
String? defaultExportFormat,
|
||||
bool? includeMetadata,
|
||||
}) {
|
||||
return EditorSettings(
|
||||
fontSize: fontSize ?? this.fontSize,
|
||||
fontFamily: fontFamily ?? this.fontFamily,
|
||||
fontWeight: fontWeight ?? this.fontWeight,
|
||||
lineSpacing: lineSpacing ?? this.lineSpacing,
|
||||
letterSpacing: letterSpacing ?? this.letterSpacing,
|
||||
paddingHorizontal: paddingHorizontal ?? this.paddingHorizontal,
|
||||
paddingVertical: paddingVertical ?? this.paddingVertical,
|
||||
paragraphSpacing: paragraphSpacing ?? this.paragraphSpacing,
|
||||
indentSize: indentSize ?? this.indentSize,
|
||||
autoSaveEnabled: autoSaveEnabled ?? this.autoSaveEnabled,
|
||||
autoSaveIntervalMinutes: autoSaveIntervalMinutes ?? this.autoSaveIntervalMinutes,
|
||||
spellCheckEnabled: spellCheckEnabled ?? this.spellCheckEnabled,
|
||||
showWordCount: showWordCount ?? this.showWordCount,
|
||||
showLineNumbers: showLineNumbers ?? this.showLineNumbers,
|
||||
highlightActiveLine: highlightActiveLine ?? this.highlightActiveLine,
|
||||
darkModeEnabled: darkModeEnabled ?? this.darkModeEnabled,
|
||||
showMiniMap: showMiniMap ?? this.showMiniMap,
|
||||
smoothScrolling: smoothScrolling ?? this.smoothScrolling,
|
||||
fadeInAnimation: fadeInAnimation ?? this.fadeInAnimation,
|
||||
themeVariant: themeVariant ?? this.themeVariant,
|
||||
maxLineWidth: maxLineWidth ?? this.maxLineWidth,
|
||||
minEditorHeight: minEditorHeight ?? this.minEditorHeight,
|
||||
useTypewriterMode: useTypewriterMode ?? this.useTypewriterMode,
|
||||
cursorBlinkRate: cursorBlinkRate ?? this.cursorBlinkRate,
|
||||
selectionHighlightColor: selectionHighlightColor ?? this.selectionHighlightColor,
|
||||
enableVimMode: enableVimMode ?? this.enableVimMode,
|
||||
defaultExportFormat: defaultExportFormat ?? this.defaultExportFormat,
|
||||
includeMetadata: includeMetadata ?? this.includeMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
/// 转换为Map(用于持久化存储)
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'fontSize': fontSize,
|
||||
'fontFamily': fontFamily,
|
||||
'fontWeight': fontWeight.index,
|
||||
'lineSpacing': lineSpacing,
|
||||
'letterSpacing': letterSpacing,
|
||||
'paddingHorizontal': paddingHorizontal,
|
||||
'paddingVertical': paddingVertical,
|
||||
'paragraphSpacing': paragraphSpacing,
|
||||
'indentSize': indentSize,
|
||||
'autoSaveEnabled': autoSaveEnabled,
|
||||
'autoSaveIntervalMinutes': autoSaveIntervalMinutes,
|
||||
'spellCheckEnabled': spellCheckEnabled,
|
||||
'showWordCount': showWordCount,
|
||||
'showLineNumbers': showLineNumbers,
|
||||
'highlightActiveLine': highlightActiveLine,
|
||||
'darkModeEnabled': darkModeEnabled,
|
||||
'showMiniMap': showMiniMap,
|
||||
'smoothScrolling': smoothScrolling,
|
||||
'fadeInAnimation': fadeInAnimation,
|
||||
'themeVariant': themeVariant,
|
||||
'maxLineWidth': maxLineWidth,
|
||||
'minEditorHeight': minEditorHeight,
|
||||
'useTypewriterMode': useTypewriterMode,
|
||||
'cursorBlinkRate': cursorBlinkRate,
|
||||
'selectionHighlightColor': selectionHighlightColor,
|
||||
'enableVimMode': enableVimMode,
|
||||
'defaultExportFormat': defaultExportFormat,
|
||||
'includeMetadata': includeMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
/// 从Map创建(用于持久化恢复)
|
||||
factory EditorSettings.fromMap(Map<String, dynamic> map) {
|
||||
// 🚀 修复:安全地转换fontWeight,处理String和int类型
|
||||
int fontWeightIndex = 3; // 默认值 FontWeight.normal
|
||||
if (map['fontWeight'] != null) {
|
||||
if (map['fontWeight'] is int) {
|
||||
fontWeightIndex = map['fontWeight'];
|
||||
} else if (map['fontWeight'] is String) {
|
||||
fontWeightIndex = int.tryParse(map['fontWeight']) ?? 3;
|
||||
}
|
||||
}
|
||||
|
||||
// 🚀 修复:安全地转换selectionHighlightColor,处理String和int类型
|
||||
int selectionColor = 0xFF2196F3; // 默认蓝色
|
||||
if (map['selectionHighlightColor'] != null) {
|
||||
if (map['selectionHighlightColor'] is int) {
|
||||
selectionColor = map['selectionHighlightColor'];
|
||||
} else if (map['selectionHighlightColor'] is String) {
|
||||
selectionColor = int.tryParse(map['selectionHighlightColor']) ?? 0xFF2196F3;
|
||||
}
|
||||
}
|
||||
|
||||
// 🚀 修复:安全地转换autoSaveIntervalMinutes,处理String和int类型
|
||||
int autoSaveInterval = 5; // 默认值
|
||||
if (map['autoSaveIntervalMinutes'] != null) {
|
||||
if (map['autoSaveIntervalMinutes'] is int) {
|
||||
autoSaveInterval = map['autoSaveIntervalMinutes'];
|
||||
} else if (map['autoSaveIntervalMinutes'] is String) {
|
||||
autoSaveInterval = int.tryParse(map['autoSaveIntervalMinutes']) ?? 5;
|
||||
}
|
||||
}
|
||||
|
||||
return EditorSettings(
|
||||
fontSize: map['fontSize']?.toDouble() ?? 16.0,
|
||||
fontFamily: map['fontFamily'] ?? 'Roboto',
|
||||
fontWeight: FontWeight.values[fontWeightIndex.clamp(0, FontWeight.values.length - 1)],
|
||||
lineSpacing: map['lineSpacing']?.toDouble() ?? 1.5,
|
||||
letterSpacing: map['letterSpacing']?.toDouble() ?? 0.0,
|
||||
paddingHorizontal: map['paddingHorizontal']?.toDouble() ?? 16.0,
|
||||
paddingVertical: map['paddingVertical']?.toDouble() ?? 12.0,
|
||||
paragraphSpacing: map['paragraphSpacing']?.toDouble() ?? 8.0,
|
||||
indentSize: map['indentSize']?.toDouble() ?? 32.0,
|
||||
autoSaveEnabled: map['autoSaveEnabled'] ?? true,
|
||||
autoSaveIntervalMinutes: autoSaveInterval,
|
||||
spellCheckEnabled: map['spellCheckEnabled'] ?? true,
|
||||
showWordCount: map['showWordCount'] ?? true,
|
||||
showLineNumbers: map['showLineNumbers'] ?? false,
|
||||
highlightActiveLine: map['highlightActiveLine'] ?? true,
|
||||
darkModeEnabled: map['darkModeEnabled'] ?? false,
|
||||
showMiniMap: map['showMiniMap'] ?? false,
|
||||
smoothScrolling: map['smoothScrolling'] ?? true,
|
||||
fadeInAnimation: map['fadeInAnimation'] ?? true,
|
||||
themeVariant: (map['themeVariant'] as String?) ?? 'monochrome',
|
||||
maxLineWidth: map['maxLineWidth']?.toDouble() ?? 1500.0,
|
||||
minEditorHeight: map['minEditorHeight']?.toDouble() ?? 1200.0,
|
||||
useTypewriterMode: map['useTypewriterMode'] ?? false,
|
||||
cursorBlinkRate: map['cursorBlinkRate']?.toDouble() ?? 1.0,
|
||||
selectionHighlightColor: selectionColor,
|
||||
enableVimMode: map['enableVimMode'] ?? false,
|
||||
defaultExportFormat: map['defaultExportFormat'] ?? 'markdown',
|
||||
includeMetadata: map['includeMetadata'] ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取可用的字体列表
|
||||
static List<String> get availableFontFamilies => [
|
||||
'Roboto',
|
||||
'serif', // 中文友好的衬线字体
|
||||
'sans-serif', // 中文友好的无衬线字体
|
||||
'monospace',
|
||||
'Noto Sans SC', // Google Noto 简体中文字体
|
||||
'PingFang SC', // 苹果中文字体
|
||||
'Microsoft YaHei', // 微软雅黑
|
||||
'SimHei', // 黑体
|
||||
'SimSun', // 宋体
|
||||
'Helvetica',
|
||||
'Times New Roman',
|
||||
'Courier New',
|
||||
'Georgia',
|
||||
'Verdana',
|
||||
'Arial',
|
||||
];
|
||||
|
||||
/// 获取可用的字体粗细选项
|
||||
static List<FontWeight> get availableFontWeights => [
|
||||
FontWeight.w300,
|
||||
FontWeight.w400,
|
||||
FontWeight.w500,
|
||||
FontWeight.w600,
|
||||
FontWeight.w700,
|
||||
];
|
||||
|
||||
/// 获取可用的导出格式
|
||||
static List<String> get availableExportFormats => [
|
||||
'markdown',
|
||||
'docx',
|
||||
'pdf',
|
||||
'txt',
|
||||
'html',
|
||||
];
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is EditorSettings &&
|
||||
other.fontSize == fontSize &&
|
||||
other.fontFamily == fontFamily &&
|
||||
other.fontWeight == fontWeight &&
|
||||
other.lineSpacing == lineSpacing &&
|
||||
other.letterSpacing == letterSpacing &&
|
||||
other.paddingHorizontal == paddingHorizontal &&
|
||||
other.paddingVertical == paddingVertical &&
|
||||
other.paragraphSpacing == paragraphSpacing &&
|
||||
other.indentSize == indentSize &&
|
||||
other.autoSaveEnabled == autoSaveEnabled &&
|
||||
other.autoSaveIntervalMinutes == autoSaveIntervalMinutes &&
|
||||
other.spellCheckEnabled == spellCheckEnabled &&
|
||||
other.showWordCount == showWordCount &&
|
||||
other.showLineNumbers == showLineNumbers &&
|
||||
other.highlightActiveLine == highlightActiveLine &&
|
||||
other.darkModeEnabled == darkModeEnabled &&
|
||||
other.showMiniMap == showMiniMap &&
|
||||
other.smoothScrolling == smoothScrolling &&
|
||||
other.fadeInAnimation == fadeInAnimation &&
|
||||
other.themeVariant == themeVariant &&
|
||||
other.maxLineWidth == maxLineWidth &&
|
||||
other.minEditorHeight == minEditorHeight &&
|
||||
other.useTypewriterMode == useTypewriterMode &&
|
||||
other.cursorBlinkRate == cursorBlinkRate &&
|
||||
other.selectionHighlightColor == selectionHighlightColor &&
|
||||
other.enableVimMode == enableVimMode &&
|
||||
other.defaultExportFormat == defaultExportFormat &&
|
||||
other.includeMetadata == includeMetadata;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hashAll([
|
||||
fontSize,
|
||||
fontFamily,
|
||||
fontWeight,
|
||||
lineSpacing,
|
||||
letterSpacing,
|
||||
paddingHorizontal,
|
||||
paddingVertical,
|
||||
paragraphSpacing,
|
||||
indentSize,
|
||||
autoSaveEnabled,
|
||||
autoSaveIntervalMinutes,
|
||||
spellCheckEnabled,
|
||||
showWordCount,
|
||||
showLineNumbers,
|
||||
highlightActiveLine,
|
||||
darkModeEnabled,
|
||||
showMiniMap,
|
||||
smoothScrolling,
|
||||
fadeInAnimation,
|
||||
themeVariant,
|
||||
maxLineWidth,
|
||||
minEditorHeight,
|
||||
useTypewriterMode,
|
||||
cursorBlinkRate,
|
||||
selectionHighlightColor,
|
||||
enableVimMode,
|
||||
defaultExportFormat,
|
||||
includeMetadata,
|
||||
]);
|
||||
}
|
||||
|
||||
/// 🚀 新增:转换为JSON(用于API调用)
|
||||
Map<String, dynamic> toJson() {
|
||||
return toMap();
|
||||
}
|
||||
|
||||
/// 🚀 新增:从JSON创建(用于API响应)
|
||||
factory EditorSettings.fromJson(Map<String, dynamic> json) {
|
||||
return EditorSettings.fromMap(json);
|
||||
}
|
||||
}
|
||||
57
AINoval/lib/models/import_status.dart
Normal file
57
AINoval/lib/models/import_status.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
/// 小说导入状态模型
|
||||
class ImportStatus {
|
||||
/// 从JSON创建实例
|
||||
factory ImportStatus.fromJson(Map<String, dynamic> json) {
|
||||
return ImportStatus(
|
||||
status: json['status'] as String,
|
||||
message: json['message'] as String,
|
||||
progress: (json['progress'] as num?)?.toDouble(),
|
||||
currentStep: json['currentStep'] as String?,
|
||||
processedChapters: json['processedChapters'] as int?,
|
||||
totalChapters: json['totalChapters'] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建导入状态
|
||||
ImportStatus({
|
||||
required this.status,
|
||||
required this.message,
|
||||
this.progress,
|
||||
this.currentStep,
|
||||
this.processedChapters,
|
||||
this.totalChapters,
|
||||
});
|
||||
|
||||
/// 导入状态 (PROCESSING, SAVING, INDEXING, COMPLETED, FAILED, ERROR)
|
||||
final String status;
|
||||
|
||||
/// 状态消息
|
||||
final String message;
|
||||
|
||||
/// 导入进度 (0.0 - 1.0)
|
||||
final double? progress;
|
||||
|
||||
/// 当前步骤描述
|
||||
final String? currentStep;
|
||||
|
||||
/// 已处理章节数
|
||||
final int? processedChapters;
|
||||
|
||||
/// 总章节数
|
||||
final int? totalChapters;
|
||||
|
||||
/// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'status': status,
|
||||
'message': message,
|
||||
if (progress != null) 'progress': progress,
|
||||
if (currentStep != null) 'currentStep': currentStep,
|
||||
if (processedChapters != null) 'processedChapters': processedChapters,
|
||||
if (totalChapters != null) 'totalChapters': totalChapters,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'ImportStatus{status: $status, message: $message, progress: $progress, currentStep: $currentStep, processedChapters: $processedChapters, totalChapters: $totalChapters}';
|
||||
}
|
||||
48
AINoval/lib/models/model_info.dart
Normal file
48
AINoval/lib/models/model_info.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Represents detailed information about an AI model provided by the backend.
|
||||
@immutable
|
||||
class ModelInfo extends Equatable {
|
||||
final String id; // Usually the unique model identifier (e.g., "gpt-4o")
|
||||
final String name; // User-friendly name (might be the same as id or different)
|
||||
final String provider;
|
||||
final String? description;
|
||||
final int? maxTokens;
|
||||
// Add other fields as needed based on backend response (e.g., pricing)
|
||||
// final double? unifiedPrice;
|
||||
|
||||
const ModelInfo({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.provider,
|
||||
this.description,
|
||||
this.maxTokens,
|
||||
// this.unifiedPrice,
|
||||
});
|
||||
|
||||
factory ModelInfo.fromJson(Map<String, dynamic> json) {
|
||||
return ModelInfo(
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? json['id'] as String? ?? '', // Fallback name to id
|
||||
provider: json['provider'] as String? ?? '',
|
||||
description: json['description'] as String?,
|
||||
maxTokens: json['maxTokens'] as int?,
|
||||
// unifiedPrice: (json['unifiedPrice'] as num?)?.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'provider': provider,
|
||||
'description': description,
|
||||
'maxTokens': maxTokens,
|
||||
// 'unifiedPrice': unifiedPrice,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, name, provider, description, maxTokens /*, unifiedPrice*/];
|
||||
}
|
||||
334
AINoval/lib/models/next_outline/next_outline_dto.dart
Normal file
334
AINoval/lib/models/next_outline/next_outline_dto.dart
Normal file
@@ -0,0 +1,334 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
/// 生成剧情大纲请求
|
||||
class GenerateNextOutlinesRequest {
|
||||
/// 上下文开始章节ID
|
||||
final String? startChapterId;
|
||||
|
||||
/// 上下文结束章节ID
|
||||
final String? endChapterId;
|
||||
|
||||
/// 生成选项数量
|
||||
final int numOptions;
|
||||
|
||||
/// 作者引导
|
||||
final String? authorGuidance;
|
||||
|
||||
/// 选定的AI模型配置ID列表
|
||||
final List<String>? selectedConfigIds;
|
||||
|
||||
/// 重新生成提示(用于全局重新生成)
|
||||
final String? regenerateHint;
|
||||
|
||||
GenerateNextOutlinesRequest({
|
||||
this.startChapterId,
|
||||
this.endChapterId,
|
||||
this.numOptions = 3,
|
||||
this.authorGuidance,
|
||||
this.selectedConfigIds,
|
||||
this.regenerateHint,
|
||||
});
|
||||
|
||||
factory GenerateNextOutlinesRequest.fromJson(Map<String, dynamic> json) {
|
||||
return GenerateNextOutlinesRequest(
|
||||
startChapterId: json['startChapterId'] as String?,
|
||||
endChapterId: json['endChapterId'] as String?,
|
||||
numOptions: json['numOptions'] as int? ?? 3,
|
||||
authorGuidance: json['authorGuidance'] as String?,
|
||||
selectedConfigIds: (json['selectedConfigIds'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||
regenerateHint: json['regenerateHint'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
if (startChapterId != null) 'startChapterId': startChapterId,
|
||||
if (endChapterId != null) 'endChapterId': endChapterId,
|
||||
'numOptions': numOptions,
|
||||
if (authorGuidance != null) 'authorGuidance': authorGuidance,
|
||||
if (selectedConfigIds != null) 'selectedConfigIds': selectedConfigIds,
|
||||
if (regenerateHint != null) 'regenerateHint': regenerateHint,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成剧情大纲响应
|
||||
class GenerateNextOutlinesResponse {
|
||||
/// 生成的大纲列表
|
||||
final List<OutlineItem> outlines;
|
||||
|
||||
/// 生成时间(毫秒)
|
||||
final int generationTimeMs;
|
||||
|
||||
GenerateNextOutlinesResponse({
|
||||
required this.outlines,
|
||||
required this.generationTimeMs,
|
||||
});
|
||||
|
||||
factory GenerateNextOutlinesResponse.fromJson(Map<String, dynamic> json) {
|
||||
return GenerateNextOutlinesResponse(
|
||||
outlines: (json['outlines'] as List<dynamic>)
|
||||
.map((e) => OutlineItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
generationTimeMs: json['generationTimeMs'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'outlines': outlines.map((e) => e.toJson()).toList(),
|
||||
'generationTimeMs': generationTimeMs,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 大纲项
|
||||
class OutlineItem {
|
||||
/// 大纲ID
|
||||
final String id;
|
||||
|
||||
/// 大纲标题
|
||||
final String title;
|
||||
|
||||
/// 大纲内容
|
||||
final String content;
|
||||
|
||||
/// 是否被选中
|
||||
final bool isSelected;
|
||||
|
||||
/// 使用的模型配置ID
|
||||
final String? configId;
|
||||
|
||||
OutlineItem({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.content,
|
||||
required this.isSelected,
|
||||
this.configId,
|
||||
});
|
||||
|
||||
factory OutlineItem.fromJson(Map<String, dynamic> json) {
|
||||
return OutlineItem(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
content: json['content'] as String,
|
||||
isSelected: json['isSelected'] as bool,
|
||||
configId: json['configId'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'content': content,
|
||||
'isSelected': isSelected,
|
||||
if (configId != null) 'configId': configId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 重新生成单个剧情大纲请求
|
||||
class RegenerateOptionRequest {
|
||||
/// 选项ID
|
||||
final String optionId;
|
||||
|
||||
/// 选定的AI模型配置ID
|
||||
final String selectedConfigId;
|
||||
|
||||
/// 重新生成提示
|
||||
final String? regenerateHint;
|
||||
|
||||
RegenerateOptionRequest({
|
||||
required this.optionId,
|
||||
required this.selectedConfigId,
|
||||
this.regenerateHint,
|
||||
});
|
||||
|
||||
factory RegenerateOptionRequest.fromJson(Map<String, dynamic> json) {
|
||||
return RegenerateOptionRequest(
|
||||
optionId: json['optionId'] as String,
|
||||
selectedConfigId: json['selectedConfigId'] as String,
|
||||
regenerateHint: json['regenerateHint'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'optionId': optionId,
|
||||
'selectedConfigId': selectedConfigId,
|
||||
if (regenerateHint != null) 'regenerateHint': regenerateHint,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存剧情大纲请求
|
||||
class SaveNextOutlineRequest {
|
||||
/// 大纲ID
|
||||
final String outlineId;
|
||||
|
||||
/// 插入位置类型
|
||||
/// CHAPTER_END: 章节末尾
|
||||
/// BEFORE_SCENE: 场景之前
|
||||
/// AFTER_SCENE: 场景之后
|
||||
/// NEW_CHAPTER: 新建章节(默认)
|
||||
final String insertType;
|
||||
|
||||
/// 目标章节ID(当insertType为CHAPTER_END时使用)
|
||||
final String? targetChapterId;
|
||||
|
||||
/// 目标场景ID(当insertType为BEFORE_SCENE或AFTER_SCENE时使用)
|
||||
final String? targetSceneId;
|
||||
|
||||
/// 是否创建新场景(默认为true)
|
||||
final bool createNewScene;
|
||||
|
||||
SaveNextOutlineRequest({
|
||||
required this.outlineId,
|
||||
this.insertType = 'NEW_CHAPTER',
|
||||
this.targetChapterId,
|
||||
this.targetSceneId,
|
||||
this.createNewScene = true,
|
||||
});
|
||||
|
||||
factory SaveNextOutlineRequest.fromJson(Map<String, dynamic> json) {
|
||||
return SaveNextOutlineRequest(
|
||||
outlineId: json['outlineId'] as String,
|
||||
insertType: json['insertType'] as String? ?? 'NEW_CHAPTER',
|
||||
targetChapterId: json['targetChapterId'] as String?,
|
||||
targetSceneId: json['targetSceneId'] as String?,
|
||||
createNewScene: json['createNewScene'] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'outlineId': outlineId,
|
||||
'insertType': insertType,
|
||||
if (targetChapterId != null) 'targetChapterId': targetChapterId,
|
||||
if (targetSceneId != null) 'targetSceneId': targetSceneId,
|
||||
'createNewScene': createNewScene,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存剧情大纲响应
|
||||
class SaveNextOutlineResponse {
|
||||
/// 是否成功
|
||||
final bool success;
|
||||
|
||||
/// 保存的大纲ID
|
||||
final String outlineId;
|
||||
|
||||
/// 新创建的章节ID(如果有)
|
||||
final String? newChapterId;
|
||||
|
||||
/// 新创建的场景ID(如果有)
|
||||
final String? newSceneId;
|
||||
|
||||
/// 目标章节ID(如果指定了现有章节)
|
||||
final String? targetChapterId;
|
||||
|
||||
/// 目标场景ID(如果指定了现有场景)
|
||||
final String? targetSceneId;
|
||||
|
||||
/// 插入位置类型
|
||||
final String insertType;
|
||||
|
||||
/// 大纲标题(用于新章节标题)
|
||||
final String outlineTitle;
|
||||
|
||||
SaveNextOutlineResponse({
|
||||
required this.success,
|
||||
required this.outlineId,
|
||||
this.newChapterId,
|
||||
this.newSceneId,
|
||||
this.targetChapterId,
|
||||
this.targetSceneId,
|
||||
required this.insertType,
|
||||
required this.outlineTitle,
|
||||
});
|
||||
|
||||
factory SaveNextOutlineResponse.fromJson(Map<String, dynamic> json) {
|
||||
return SaveNextOutlineResponse(
|
||||
success: json['success'] as bool,
|
||||
outlineId: json['outlineId'] as String,
|
||||
newChapterId: json['newChapterId'] as String?,
|
||||
newSceneId: json['newSceneId'] as String?,
|
||||
targetChapterId: json['targetChapterId'] as String?,
|
||||
targetSceneId: json['targetSceneId'] as String?,
|
||||
insertType: json['insertType'] as String,
|
||||
outlineTitle: json['outlineTitle'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'success': success,
|
||||
'outlineId': outlineId,
|
||||
if (newChapterId != null) 'newChapterId': newChapterId,
|
||||
if (newSceneId != null) 'newSceneId': newSceneId,
|
||||
if (targetChapterId != null) 'targetChapterId': targetChapterId,
|
||||
if (targetSceneId != null) 'targetSceneId': targetSceneId,
|
||||
'insertType': insertType,
|
||||
'outlineTitle': outlineTitle,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 大纲生成输出结果
|
||||
class NextOutlineOutput {
|
||||
/// 大纲列表
|
||||
final List<NextOutlineDTO> outlineList;
|
||||
|
||||
/// 生成时间(毫秒)
|
||||
final int generationTimeMs;
|
||||
|
||||
/// 所选大纲索引
|
||||
final int? selectedOutlineIndex;
|
||||
|
||||
NextOutlineOutput({
|
||||
required this.outlineList,
|
||||
required this.generationTimeMs,
|
||||
this.selectedOutlineIndex,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'outlineList': outlineList.map((e) => e.toJson()).toList(),
|
||||
'generationTimeMs': generationTimeMs,
|
||||
if (selectedOutlineIndex != null) 'selectedOutlineIndex': selectedOutlineIndex,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 剧情大纲DTO
|
||||
class NextOutlineDTO {
|
||||
/// 大纲ID
|
||||
final String id;
|
||||
|
||||
/// 大纲标题
|
||||
final String title;
|
||||
|
||||
/// 大纲内容
|
||||
final String content;
|
||||
|
||||
/// 模型配置ID
|
||||
final String? configId;
|
||||
|
||||
NextOutlineDTO({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.configId,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'content': content,
|
||||
if (configId != null) 'configId': configId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
/// 剧情大纲生成的数据块
|
||||
/// 用于流式传输生成的剧情大纲选项
|
||||
class OutlineGenerationChunk {
|
||||
/// 选项ID,用于唯一标识一个剧情选项
|
||||
final String optionId;
|
||||
|
||||
/// 选项标题,AI生成的剧情选项的短标题
|
||||
final String? optionTitle;
|
||||
|
||||
/// 文本块内容,大纲内容的文本片段
|
||||
final String textChunk;
|
||||
|
||||
/// 是否为该选项的最后一个块
|
||||
final bool isFinalChunk;
|
||||
|
||||
/// 错误信息,如果生成过程中出错则包含错误信息
|
||||
final String? error;
|
||||
|
||||
OutlineGenerationChunk({
|
||||
required this.optionId,
|
||||
this.optionTitle,
|
||||
required this.textChunk,
|
||||
required this.isFinalChunk,
|
||||
this.error,
|
||||
});
|
||||
|
||||
factory OutlineGenerationChunk.fromJson(Map<String, dynamic> json) {
|
||||
return OutlineGenerationChunk(
|
||||
optionId: json['optionId'] as String? ?? '',
|
||||
optionTitle: json['optionTitle'] as String?,
|
||||
textChunk: json['textChunk'] as String? ?? '',
|
||||
isFinalChunk: json['finalChunk'] as bool? ?? false,
|
||||
error: json['error'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'optionId': optionId,
|
||||
if (optionTitle != null) 'optionTitle': optionTitle,
|
||||
'textChunk': textChunk,
|
||||
'isFinalChunk': isFinalChunk,
|
||||
if (error != null) 'error': error,
|
||||
};
|
||||
}
|
||||
}
|
||||
291
AINoval/lib/models/novel_setting_item.dart
Normal file
291
AINoval/lib/models/novel_setting_item.dart
Normal file
@@ -0,0 +1,291 @@
|
||||
import 'dart:convert';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:ainoval/models/ai_context_tracking.dart';
|
||||
import 'package:ainoval/models/setting_relationship_type.dart';
|
||||
|
||||
/// 小说设定条目模型
|
||||
class NovelSettingItem extends Equatable {
|
||||
final String? id;
|
||||
final String? novelId;
|
||||
final String? userId;
|
||||
final String name;
|
||||
final String? type;
|
||||
final String? content;
|
||||
final String? description;
|
||||
final Map<String, String>? attributes;
|
||||
final String? imageUrl;
|
||||
final List<SettingRelationship>? relationships;
|
||||
final List<String>? sceneIds;
|
||||
final int? priority;
|
||||
final String? generatedBy;
|
||||
final List<String>? tags;
|
||||
final String? status;
|
||||
final List<double>? vector;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final bool isAiSuggestion;
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
// ==================== 父子关系字段 ====================
|
||||
|
||||
/// 父设定ID(建立层级关系的核心字段)
|
||||
final String? parentId;
|
||||
|
||||
/// 子设定ID列表(冗余字段,用于快速查询)
|
||||
final List<String>? childrenIds;
|
||||
|
||||
// ==================== AI上下文追踪字段 ====================
|
||||
|
||||
/// 名称/别名追踪设置
|
||||
final NameAliasTracking nameAliasTracking;
|
||||
|
||||
/// AI上下文包含设置
|
||||
final AIContextTracking aiContextTracking;
|
||||
|
||||
/// 设定引用更新设置
|
||||
final SettingReferenceUpdate referenceUpdatePolicy;
|
||||
|
||||
const NovelSettingItem({
|
||||
this.id,
|
||||
this.novelId,
|
||||
this.userId,
|
||||
required this.name,
|
||||
this.type,
|
||||
this.content = "",
|
||||
this.description,
|
||||
this.attributes,
|
||||
this.imageUrl,
|
||||
this.relationships,
|
||||
this.sceneIds,
|
||||
this.priority,
|
||||
this.generatedBy,
|
||||
this.tags,
|
||||
this.status,
|
||||
this.vector,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.isAiSuggestion = false,
|
||||
this.metadata,
|
||||
this.parentId,
|
||||
this.childrenIds,
|
||||
this.nameAliasTracking = NameAliasTracking.track,
|
||||
this.aiContextTracking = AIContextTracking.detected,
|
||||
this.referenceUpdatePolicy = SettingReferenceUpdate.ask,
|
||||
});
|
||||
|
||||
factory NovelSettingItem.fromJson(Map<String, dynamic> json) {
|
||||
List<SettingRelationship>? relationships;
|
||||
if (json['relationships'] != null && json['relationships'] is List) {
|
||||
relationships = (json['relationships'] as List)
|
||||
.map((e) => SettingRelationship.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Map<String, String>? attributesMap;
|
||||
if (json['attributes'] != null && json['attributes'] is Map) {
|
||||
attributesMap = Map<String, String>.from(json['attributes'] as Map);
|
||||
}
|
||||
|
||||
List<String>? tagsList;
|
||||
if (json['tags'] != null && json['tags'] is List) {
|
||||
tagsList = List<String>.from(json['tags'] as List);
|
||||
}
|
||||
|
||||
List<String>? sceneIdsList;
|
||||
if (json['sceneIds'] != null && json['sceneIds'] is List) {
|
||||
sceneIdsList = List<String>.from(json['sceneIds'] as List);
|
||||
}
|
||||
|
||||
List<String>? childrenIdsList;
|
||||
if (json['childrenIds'] != null && json['childrenIds'] is List) {
|
||||
childrenIdsList = List<String>.from(json['childrenIds'] as List);
|
||||
}
|
||||
|
||||
List<double>? vectorList;
|
||||
if (json['vector'] != null && json['vector'] is List) {
|
||||
vectorList = (json['vector'] as List).map((e) => (e as num).toDouble()).toList();
|
||||
}
|
||||
|
||||
Map<String, dynamic>? metadataMap;
|
||||
if (json['metadata'] != null && json['metadata'] is Map) {
|
||||
metadataMap = Map<String, dynamic>.from(json['metadata'] as Map);
|
||||
}
|
||||
|
||||
return NovelSettingItem(
|
||||
id: json['id'] as String?,
|
||||
novelId: json['novelId'] as String?,
|
||||
userId: json['userId'] as String?,
|
||||
name: json['name'] as String? ?? '未命名设定',
|
||||
type: json['type'] as String?,
|
||||
content: json['content'] as String?,
|
||||
description: json['description'] as String?,
|
||||
attributes: attributesMap,
|
||||
imageUrl: json['imageUrl'] as String?,
|
||||
relationships: relationships,
|
||||
sceneIds: sceneIdsList,
|
||||
priority: json['priority'] as int?,
|
||||
status: json['status'] as String?,
|
||||
generatedBy: json['generatedBy'] as String?,
|
||||
tags: tagsList,
|
||||
vector: vectorList,
|
||||
createdAt: json['createdAt'] != null ? DateTime.tryParse(json['createdAt'].toString()) : null,
|
||||
updatedAt: json['updatedAt'] != null ? DateTime.tryParse(json['updatedAt'].toString()) : null,
|
||||
isAiSuggestion: json['isAiSuggestion'] as bool? ?? false,
|
||||
metadata: metadataMap,
|
||||
parentId: json['parentId'] as String?,
|
||||
childrenIds: childrenIdsList,
|
||||
nameAliasTracking: NameAliasTracking.fromValue(json['nameAliasTracking'] as String?),
|
||||
aiContextTracking: AIContextTracking.fromValue(json['aiContextTracking'] as String?),
|
||||
referenceUpdatePolicy: SettingReferenceUpdate.fromValue(json['referenceUpdatePolicy'] as String?),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
if (id != null) data['id'] = id;
|
||||
if (novelId != null) data['novelId'] = novelId;
|
||||
if (userId != null) data['userId'] = userId;
|
||||
data['name'] = name;
|
||||
if (type != null) data['type'] = type;
|
||||
if (content != null) data['content'] = content;
|
||||
if (description != null) data['description'] = description;
|
||||
if (attributes != null) data['attributes'] = attributes;
|
||||
if (imageUrl != null) data['imageUrl'] = imageUrl;
|
||||
if (relationships != null) {
|
||||
data['relationships'] = relationships!.map((e) => e.toJson()).toList();
|
||||
}
|
||||
if (sceneIds != null) data['sceneIds'] = sceneIds;
|
||||
if (priority != null) data['priority'] = priority;
|
||||
if (generatedBy != null) data['generatedBy'] = generatedBy;
|
||||
if (tags != null) data['tags'] = tags;
|
||||
if (status != null) data['status'] = status;
|
||||
if (vector != null) data['vector'] = vector;
|
||||
if (createdAt != null) data['createdAt'] = createdAt!.toIso8601String();
|
||||
if (updatedAt != null) data['updatedAt'] = updatedAt!.toIso8601String();
|
||||
data['isAiSuggestion'] = isAiSuggestion;
|
||||
if (metadata != null) data['metadata'] = metadata;
|
||||
if (parentId != null) data['parentId'] = parentId;
|
||||
if (childrenIds != null) data['childrenIds'] = childrenIds;
|
||||
data['nameAliasTracking'] = nameAliasTracking.value;
|
||||
data['aiContextTracking'] = aiContextTracking.value;
|
||||
data['referenceUpdatePolicy'] = referenceUpdatePolicy.value;
|
||||
return data;
|
||||
}
|
||||
|
||||
NovelSettingItem copyWith({
|
||||
String? id,
|
||||
String? novelId,
|
||||
String? userId,
|
||||
String? name,
|
||||
String? type,
|
||||
String? content,
|
||||
String? description,
|
||||
Map<String, String>? attributes,
|
||||
String? imageUrl,
|
||||
List<SettingRelationship>? relationships,
|
||||
List<String>? sceneIds,
|
||||
int? priority,
|
||||
String? generatedBy,
|
||||
List<String>? tags,
|
||||
String? status,
|
||||
List<double>? vector,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
bool? isAiSuggestion,
|
||||
Map<String, dynamic>? metadata,
|
||||
String? parentId,
|
||||
List<String>? childrenIds,
|
||||
NameAliasTracking? nameAliasTracking,
|
||||
AIContextTracking? aiContextTracking,
|
||||
SettingReferenceUpdate? referenceUpdatePolicy,
|
||||
}) {
|
||||
return NovelSettingItem(
|
||||
id: id ?? this.id,
|
||||
novelId: novelId ?? this.novelId,
|
||||
userId: userId ?? this.userId,
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
content: content ?? this.content,
|
||||
description: description ?? this.description,
|
||||
attributes: attributes ?? this.attributes,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
relationships: relationships ?? this.relationships,
|
||||
sceneIds: sceneIds ?? this.sceneIds,
|
||||
priority: priority ?? this.priority,
|
||||
generatedBy: generatedBy ?? this.generatedBy,
|
||||
tags: tags ?? this.tags,
|
||||
status: status ?? this.status,
|
||||
vector: vector ?? this.vector,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
isAiSuggestion: isAiSuggestion ?? this.isAiSuggestion,
|
||||
metadata: metadata ?? this.metadata,
|
||||
parentId: parentId ?? this.parentId,
|
||||
childrenIds: childrenIds ?? this.childrenIds,
|
||||
nameAliasTracking: nameAliasTracking ?? this.nameAliasTracking,
|
||||
aiContextTracking: aiContextTracking ?? this.aiContextTracking,
|
||||
referenceUpdatePolicy: referenceUpdatePolicy ?? this.referenceUpdatePolicy,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id, novelId, userId, name, type, content, description, attributes,
|
||||
imageUrl, relationships, sceneIds, priority, generatedBy, tags, status,
|
||||
vector, createdAt, updatedAt, isAiSuggestion, metadata, parentId, childrenIds,
|
||||
nameAliasTracking, aiContextTracking, referenceUpdatePolicy
|
||||
];
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return jsonEncode(toJson());
|
||||
}
|
||||
}
|
||||
|
||||
/// 设定关系模型
|
||||
class SettingRelationship extends Equatable {
|
||||
final String targetItemId;
|
||||
final SettingRelationshipType type;
|
||||
final String? description;
|
||||
final int? strength;
|
||||
final String? direction;
|
||||
final DateTime? createdAt;
|
||||
final Map<String, dynamic>? attributes;
|
||||
|
||||
const SettingRelationship({
|
||||
required this.targetItemId,
|
||||
required this.type,
|
||||
this.description,
|
||||
this.strength,
|
||||
this.direction,
|
||||
this.createdAt,
|
||||
this.attributes,
|
||||
});
|
||||
|
||||
factory SettingRelationship.fromJson(Map<String, dynamic> json) {
|
||||
return SettingRelationship(
|
||||
targetItemId: json['targetItemId'] as String,
|
||||
type: SettingRelationshipType.fromValue(json['type'] as String),
|
||||
description: json['description'] as String?,
|
||||
strength: json['strength'] as int?,
|
||||
direction: json['direction'] as String?,
|
||||
createdAt: json['createdAt'] != null ? DateTime.tryParse(json['createdAt'].toString()) : null,
|
||||
attributes: json['attributes'] != null ? Map<String, dynamic>.from(json['attributes']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['targetItemId'] = targetItemId;
|
||||
data['type'] = type.value;
|
||||
if (description != null) data['description'] = description;
|
||||
if (strength != null) data['strength'] = strength;
|
||||
if (direction != null) data['direction'] = direction;
|
||||
if (createdAt != null) data['createdAt'] = createdAt!.toIso8601String();
|
||||
if (attributes != null) data['attributes'] = attributes;
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [targetItemId, type, description, strength, direction, createdAt, attributes];
|
||||
}
|
||||
312
AINoval/lib/models/novel_snippet.dart
Normal file
312
AINoval/lib/models/novel_snippet.dart
Normal file
@@ -0,0 +1,312 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:ainoval/utils/date_time_parser.dart';
|
||||
|
||||
part 'novel_snippet.g.dart';
|
||||
|
||||
/// 小说片段模型
|
||||
@JsonSerializable()
|
||||
class NovelSnippet {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String novelId;
|
||||
final String title;
|
||||
final String content;
|
||||
final InitialGenerationInfo? initialGenerationInfo;
|
||||
final List<String>? tags;
|
||||
final String? category;
|
||||
final String? notes;
|
||||
final SnippetMetadata metadata;
|
||||
final bool isFavorite;
|
||||
final String status;
|
||||
final int version;
|
||||
|
||||
@JsonKey(fromJson: parseBackendDateTime, toJson: _dateTimeToJson)
|
||||
final DateTime createdAt;
|
||||
|
||||
@JsonKey(fromJson: parseBackendDateTime, toJson: _dateTimeToJson)
|
||||
final DateTime updatedAt;
|
||||
|
||||
const NovelSnippet({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.novelId,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.initialGenerationInfo,
|
||||
this.tags,
|
||||
this.category,
|
||||
this.notes,
|
||||
required this.metadata,
|
||||
required this.isFavorite,
|
||||
required this.status,
|
||||
required this.version,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory NovelSnippet.fromJson(Map<String, dynamic> json) =>
|
||||
_$NovelSnippetFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$NovelSnippetToJson(this);
|
||||
|
||||
static String _dateTimeToJson(DateTime dateTime) => dateTime.toIso8601String();
|
||||
|
||||
NovelSnippet copyWith({
|
||||
String? id,
|
||||
String? userId,
|
||||
String? novelId,
|
||||
String? title,
|
||||
String? content,
|
||||
InitialGenerationInfo? initialGenerationInfo,
|
||||
List<String>? tags,
|
||||
String? category,
|
||||
String? notes,
|
||||
SnippetMetadata? metadata,
|
||||
bool? isFavorite,
|
||||
String? status,
|
||||
int? version,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return NovelSnippet(
|
||||
id: id ?? this.id,
|
||||
userId: userId ?? this.userId,
|
||||
novelId: novelId ?? this.novelId,
|
||||
title: title ?? this.title,
|
||||
content: content ?? this.content,
|
||||
initialGenerationInfo: initialGenerationInfo ?? this.initialGenerationInfo,
|
||||
tags: tags ?? this.tags,
|
||||
category: category ?? this.category,
|
||||
notes: notes ?? this.notes,
|
||||
metadata: metadata ?? this.metadata,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
status: status ?? this.status,
|
||||
version: version ?? this.version,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始生成信息
|
||||
@JsonSerializable()
|
||||
class InitialGenerationInfo {
|
||||
final String? sourceChapterId;
|
||||
final String? sourceSceneId;
|
||||
|
||||
const InitialGenerationInfo({
|
||||
this.sourceChapterId,
|
||||
this.sourceSceneId,
|
||||
});
|
||||
|
||||
factory InitialGenerationInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$InitialGenerationInfoFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$InitialGenerationInfoToJson(this);
|
||||
}
|
||||
|
||||
/// 片段元数据
|
||||
@JsonSerializable()
|
||||
class SnippetMetadata {
|
||||
final int wordCount;
|
||||
final int characterCount;
|
||||
final int viewCount;
|
||||
final int sortWeight;
|
||||
|
||||
@JsonKey(fromJson: _parseOptionalDateTime, toJson: _optionalDateTimeToJson)
|
||||
final DateTime? lastViewedAt;
|
||||
|
||||
const SnippetMetadata({
|
||||
required this.wordCount,
|
||||
required this.characterCount,
|
||||
required this.viewCount,
|
||||
required this.sortWeight,
|
||||
this.lastViewedAt,
|
||||
});
|
||||
|
||||
factory SnippetMetadata.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnippetMetadataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SnippetMetadataToJson(this);
|
||||
|
||||
static DateTime? _parseOptionalDateTime(dynamic value) {
|
||||
return value == null ? null : parseBackendDateTime(value);
|
||||
}
|
||||
|
||||
static String? _optionalDateTimeToJson(DateTime? dateTime) {
|
||||
return dateTime?.toIso8601String();
|
||||
}
|
||||
}
|
||||
|
||||
/// 小说片段历史记录
|
||||
@JsonSerializable()
|
||||
class NovelSnippetHistory {
|
||||
final String id;
|
||||
final String snippetId;
|
||||
final String userId;
|
||||
final String operationType;
|
||||
final int version;
|
||||
final String? beforeTitle;
|
||||
final String? afterTitle;
|
||||
final String? beforeContent;
|
||||
final String? afterContent;
|
||||
final String? changeDescription;
|
||||
|
||||
@JsonKey(fromJson: parseBackendDateTime, toJson: _dateTimeToJson)
|
||||
final DateTime createdAt;
|
||||
|
||||
const NovelSnippetHistory({
|
||||
required this.id,
|
||||
required this.snippetId,
|
||||
required this.userId,
|
||||
required this.operationType,
|
||||
required this.version,
|
||||
this.beforeTitle,
|
||||
this.afterTitle,
|
||||
this.beforeContent,
|
||||
this.afterContent,
|
||||
this.changeDescription,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
factory NovelSnippetHistory.fromJson(Map<String, dynamic> json) =>
|
||||
_$NovelSnippetHistoryFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$NovelSnippetHistoryToJson(this);
|
||||
|
||||
static String _dateTimeToJson(DateTime dateTime) => dateTime.toIso8601String();
|
||||
}
|
||||
|
||||
/// 分页结果包装类
|
||||
@JsonSerializable(genericArgumentFactories: true)
|
||||
class SnippetPageResult<T> {
|
||||
final List<T> content;
|
||||
final int page;
|
||||
final int size;
|
||||
final int totalElements;
|
||||
final int totalPages;
|
||||
final bool hasNext;
|
||||
final bool hasPrevious;
|
||||
|
||||
const SnippetPageResult({
|
||||
required this.content,
|
||||
required this.page,
|
||||
required this.size,
|
||||
required this.totalElements,
|
||||
required this.totalPages,
|
||||
required this.hasNext,
|
||||
required this.hasPrevious,
|
||||
});
|
||||
|
||||
factory SnippetPageResult.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
T Function(Object? json) fromJsonT,
|
||||
) =>
|
||||
_$SnippetPageResultFromJson(json, fromJsonT);
|
||||
|
||||
Map<String, dynamic> toJson(Object? Function(T value) toJsonT) =>
|
||||
_$SnippetPageResultToJson(this, toJsonT);
|
||||
}
|
||||
|
||||
/// 创建片段请求
|
||||
@JsonSerializable()
|
||||
class CreateSnippetRequest {
|
||||
final String novelId;
|
||||
final String title;
|
||||
final String content;
|
||||
final String? sourceChapterId;
|
||||
final String? sourceSceneId;
|
||||
final List<String>? tags;
|
||||
final String? category;
|
||||
final String? notes;
|
||||
|
||||
const CreateSnippetRequest({
|
||||
required this.novelId,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.sourceChapterId,
|
||||
this.sourceSceneId,
|
||||
this.tags,
|
||||
this.category,
|
||||
this.notes,
|
||||
});
|
||||
|
||||
factory CreateSnippetRequest.fromJson(Map<String, dynamic> json) =>
|
||||
_$CreateSnippetRequestFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$CreateSnippetRequestToJson(this);
|
||||
}
|
||||
|
||||
/// 更新片段内容请求
|
||||
@JsonSerializable()
|
||||
class UpdateSnippetContentRequest {
|
||||
final String snippetId;
|
||||
final String content;
|
||||
final String? changeDescription;
|
||||
|
||||
const UpdateSnippetContentRequest({
|
||||
required this.snippetId,
|
||||
required this.content,
|
||||
this.changeDescription,
|
||||
});
|
||||
|
||||
factory UpdateSnippetContentRequest.fromJson(Map<String, dynamic> json) =>
|
||||
_$UpdateSnippetContentRequestFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$UpdateSnippetContentRequestToJson(this);
|
||||
}
|
||||
|
||||
/// 更新片段标题请求
|
||||
@JsonSerializable()
|
||||
class UpdateSnippetTitleRequest {
|
||||
final String snippetId;
|
||||
final String title;
|
||||
final String? changeDescription;
|
||||
|
||||
const UpdateSnippetTitleRequest({
|
||||
required this.snippetId,
|
||||
required this.title,
|
||||
this.changeDescription,
|
||||
});
|
||||
|
||||
factory UpdateSnippetTitleRequest.fromJson(Map<String, dynamic> json) =>
|
||||
_$UpdateSnippetTitleRequestFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$UpdateSnippetTitleRequestToJson(this);
|
||||
}
|
||||
|
||||
/// 更新收藏状态请求
|
||||
@JsonSerializable()
|
||||
class UpdateSnippetFavoriteRequest {
|
||||
final String snippetId;
|
||||
final bool isFavorite;
|
||||
|
||||
const UpdateSnippetFavoriteRequest({
|
||||
required this.snippetId,
|
||||
required this.isFavorite,
|
||||
});
|
||||
|
||||
factory UpdateSnippetFavoriteRequest.fromJson(Map<String, dynamic> json) =>
|
||||
_$UpdateSnippetFavoriteRequestFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$UpdateSnippetFavoriteRequestToJson(this);
|
||||
}
|
||||
|
||||
/// 回退版本请求
|
||||
@JsonSerializable()
|
||||
class RevertSnippetVersionRequest {
|
||||
final String snippetId;
|
||||
final int version;
|
||||
final String? changeDescription;
|
||||
|
||||
const RevertSnippetVersionRequest({
|
||||
required this.snippetId,
|
||||
required this.version,
|
||||
this.changeDescription,
|
||||
});
|
||||
|
||||
factory RevertSnippetVersionRequest.fromJson(Map<String, dynamic> json) =>
|
||||
_$RevertSnippetVersionRequestFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$RevertSnippetVersionRequestToJson(this);
|
||||
}
|
||||
386
AINoval/lib/models/novel_snippet.g.dart
Normal file
386
AINoval/lib/models/novel_snippet.g.dart
Normal file
@@ -0,0 +1,386 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'novel_snippet.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
NovelSnippet _$NovelSnippetFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'NovelSnippet',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = NovelSnippet(
|
||||
id: $checkedConvert('id', (v) => v as String),
|
||||
userId: $checkedConvert('userId', (v) => v as String),
|
||||
novelId: $checkedConvert('novelId', (v) => v as String),
|
||||
title: $checkedConvert('title', (v) => v as String),
|
||||
content: $checkedConvert('content', (v) => v as String),
|
||||
initialGenerationInfo: $checkedConvert(
|
||||
'initialGenerationInfo',
|
||||
(v) => v == null
|
||||
? null
|
||||
: InitialGenerationInfo.fromJson(v as Map<String, dynamic>)),
|
||||
tags: $checkedConvert('tags',
|
||||
(v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
|
||||
category: $checkedConvert('category', (v) => v as String?),
|
||||
notes: $checkedConvert('notes', (v) => v as String?),
|
||||
metadata: $checkedConvert('metadata',
|
||||
(v) => SnippetMetadata.fromJson(v as Map<String, dynamic>)),
|
||||
isFavorite: $checkedConvert('isFavorite', (v) => v as bool),
|
||||
status: $checkedConvert('status', (v) => v as String),
|
||||
version: $checkedConvert('version', (v) => (v as num).toInt()),
|
||||
createdAt:
|
||||
$checkedConvert('createdAt', (v) => parseBackendDateTime(v)),
|
||||
updatedAt:
|
||||
$checkedConvert('updatedAt', (v) => parseBackendDateTime(v)),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$NovelSnippetToJson(NovelSnippet instance) {
|
||||
final val = <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'userId': instance.userId,
|
||||
'novelId': instance.novelId,
|
||||
'title': instance.title,
|
||||
'content': instance.content,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull(
|
||||
'initialGenerationInfo', instance.initialGenerationInfo?.toJson());
|
||||
writeNotNull('tags', instance.tags);
|
||||
writeNotNull('category', instance.category);
|
||||
writeNotNull('notes', instance.notes);
|
||||
val['metadata'] = instance.metadata.toJson();
|
||||
val['isFavorite'] = instance.isFavorite;
|
||||
val['status'] = instance.status;
|
||||
val['version'] = instance.version;
|
||||
val['createdAt'] = NovelSnippet._dateTimeToJson(instance.createdAt);
|
||||
val['updatedAt'] = NovelSnippet._dateTimeToJson(instance.updatedAt);
|
||||
return val;
|
||||
}
|
||||
|
||||
InitialGenerationInfo _$InitialGenerationInfoFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'InitialGenerationInfo',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = InitialGenerationInfo(
|
||||
sourceChapterId:
|
||||
$checkedConvert('sourceChapterId', (v) => v as String?),
|
||||
sourceSceneId: $checkedConvert('sourceSceneId', (v) => v as String?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$InitialGenerationInfoToJson(
|
||||
InitialGenerationInfo instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('sourceChapterId', instance.sourceChapterId);
|
||||
writeNotNull('sourceSceneId', instance.sourceSceneId);
|
||||
return val;
|
||||
}
|
||||
|
||||
SnippetMetadata _$SnippetMetadataFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'SnippetMetadata',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = SnippetMetadata(
|
||||
wordCount: $checkedConvert('wordCount', (v) => (v as num).toInt()),
|
||||
characterCount:
|
||||
$checkedConvert('characterCount', (v) => (v as num).toInt()),
|
||||
viewCount: $checkedConvert('viewCount', (v) => (v as num).toInt()),
|
||||
sortWeight: $checkedConvert('sortWeight', (v) => (v as num).toInt()),
|
||||
lastViewedAt: $checkedConvert(
|
||||
'lastViewedAt', (v) => SnippetMetadata._parseOptionalDateTime(v)),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnippetMetadataToJson(SnippetMetadata instance) {
|
||||
final val = <String, dynamic>{
|
||||
'wordCount': instance.wordCount,
|
||||
'characterCount': instance.characterCount,
|
||||
'viewCount': instance.viewCount,
|
||||
'sortWeight': instance.sortWeight,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('lastViewedAt',
|
||||
SnippetMetadata._optionalDateTimeToJson(instance.lastViewedAt));
|
||||
return val;
|
||||
}
|
||||
|
||||
NovelSnippetHistory _$NovelSnippetHistoryFromJson(Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'NovelSnippetHistory',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = NovelSnippetHistory(
|
||||
id: $checkedConvert('id', (v) => v as String),
|
||||
snippetId: $checkedConvert('snippetId', (v) => v as String),
|
||||
userId: $checkedConvert('userId', (v) => v as String),
|
||||
operationType: $checkedConvert('operationType', (v) => v as String),
|
||||
version: $checkedConvert('version', (v) => (v as num).toInt()),
|
||||
beforeTitle: $checkedConvert('beforeTitle', (v) => v as String?),
|
||||
afterTitle: $checkedConvert('afterTitle', (v) => v as String?),
|
||||
beforeContent: $checkedConvert('beforeContent', (v) => v as String?),
|
||||
afterContent: $checkedConvert('afterContent', (v) => v as String?),
|
||||
changeDescription:
|
||||
$checkedConvert('changeDescription', (v) => v as String?),
|
||||
createdAt:
|
||||
$checkedConvert('createdAt', (v) => parseBackendDateTime(v)),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$NovelSnippetHistoryToJson(NovelSnippetHistory instance) {
|
||||
final val = <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'snippetId': instance.snippetId,
|
||||
'userId': instance.userId,
|
||||
'operationType': instance.operationType,
|
||||
'version': instance.version,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('beforeTitle', instance.beforeTitle);
|
||||
writeNotNull('afterTitle', instance.afterTitle);
|
||||
writeNotNull('beforeContent', instance.beforeContent);
|
||||
writeNotNull('afterContent', instance.afterContent);
|
||||
writeNotNull('changeDescription', instance.changeDescription);
|
||||
val['createdAt'] = NovelSnippetHistory._dateTimeToJson(instance.createdAt);
|
||||
return val;
|
||||
}
|
||||
|
||||
SnippetPageResult<T> _$SnippetPageResultFromJson<T>(
|
||||
Map<String, dynamic> json,
|
||||
T Function(Object? json) fromJsonT,
|
||||
) =>
|
||||
$checkedCreate(
|
||||
'SnippetPageResult',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = SnippetPageResult<T>(
|
||||
content: $checkedConvert(
|
||||
'content', (v) => (v as List<dynamic>).map(fromJsonT).toList()),
|
||||
page: $checkedConvert('page', (v) => (v as num).toInt()),
|
||||
size: $checkedConvert('size', (v) => (v as num).toInt()),
|
||||
totalElements:
|
||||
$checkedConvert('totalElements', (v) => (v as num).toInt()),
|
||||
totalPages: $checkedConvert('totalPages', (v) => (v as num).toInt()),
|
||||
hasNext: $checkedConvert('hasNext', (v) => v as bool),
|
||||
hasPrevious: $checkedConvert('hasPrevious', (v) => v as bool),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnippetPageResultToJson<T>(
|
||||
SnippetPageResult<T> instance,
|
||||
Object? Function(T value) toJsonT,
|
||||
) =>
|
||||
<String, dynamic>{
|
||||
'content': instance.content.map(toJsonT).toList(),
|
||||
'page': instance.page,
|
||||
'size': instance.size,
|
||||
'totalElements': instance.totalElements,
|
||||
'totalPages': instance.totalPages,
|
||||
'hasNext': instance.hasNext,
|
||||
'hasPrevious': instance.hasPrevious,
|
||||
};
|
||||
|
||||
CreateSnippetRequest _$CreateSnippetRequestFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'CreateSnippetRequest',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = CreateSnippetRequest(
|
||||
novelId: $checkedConvert('novelId', (v) => v as String),
|
||||
title: $checkedConvert('title', (v) => v as String),
|
||||
content: $checkedConvert('content', (v) => v as String),
|
||||
sourceChapterId:
|
||||
$checkedConvert('sourceChapterId', (v) => v as String?),
|
||||
sourceSceneId: $checkedConvert('sourceSceneId', (v) => v as String?),
|
||||
tags: $checkedConvert('tags',
|
||||
(v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
|
||||
category: $checkedConvert('category', (v) => v as String?),
|
||||
notes: $checkedConvert('notes', (v) => v as String?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CreateSnippetRequestToJson(
|
||||
CreateSnippetRequest instance) {
|
||||
final val = <String, dynamic>{
|
||||
'novelId': instance.novelId,
|
||||
'title': instance.title,
|
||||
'content': instance.content,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('sourceChapterId', instance.sourceChapterId);
|
||||
writeNotNull('sourceSceneId', instance.sourceSceneId);
|
||||
writeNotNull('tags', instance.tags);
|
||||
writeNotNull('category', instance.category);
|
||||
writeNotNull('notes', instance.notes);
|
||||
return val;
|
||||
}
|
||||
|
||||
UpdateSnippetContentRequest _$UpdateSnippetContentRequestFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'UpdateSnippetContentRequest',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = UpdateSnippetContentRequest(
|
||||
snippetId: $checkedConvert('snippetId', (v) => v as String),
|
||||
content: $checkedConvert('content', (v) => v as String),
|
||||
changeDescription:
|
||||
$checkedConvert('changeDescription', (v) => v as String?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UpdateSnippetContentRequestToJson(
|
||||
UpdateSnippetContentRequest instance) {
|
||||
final val = <String, dynamic>{
|
||||
'snippetId': instance.snippetId,
|
||||
'content': instance.content,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('changeDescription', instance.changeDescription);
|
||||
return val;
|
||||
}
|
||||
|
||||
UpdateSnippetTitleRequest _$UpdateSnippetTitleRequestFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'UpdateSnippetTitleRequest',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = UpdateSnippetTitleRequest(
|
||||
snippetId: $checkedConvert('snippetId', (v) => v as String),
|
||||
title: $checkedConvert('title', (v) => v as String),
|
||||
changeDescription:
|
||||
$checkedConvert('changeDescription', (v) => v as String?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UpdateSnippetTitleRequestToJson(
|
||||
UpdateSnippetTitleRequest instance) {
|
||||
final val = <String, dynamic>{
|
||||
'snippetId': instance.snippetId,
|
||||
'title': instance.title,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('changeDescription', instance.changeDescription);
|
||||
return val;
|
||||
}
|
||||
|
||||
UpdateSnippetFavoriteRequest _$UpdateSnippetFavoriteRequestFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'UpdateSnippetFavoriteRequest',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = UpdateSnippetFavoriteRequest(
|
||||
snippetId: $checkedConvert('snippetId', (v) => v as String),
|
||||
isFavorite: $checkedConvert('isFavorite', (v) => v as bool),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UpdateSnippetFavoriteRequestToJson(
|
||||
UpdateSnippetFavoriteRequest instance) =>
|
||||
<String, dynamic>{
|
||||
'snippetId': instance.snippetId,
|
||||
'isFavorite': instance.isFavorite,
|
||||
};
|
||||
|
||||
RevertSnippetVersionRequest _$RevertSnippetVersionRequestFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
$checkedCreate(
|
||||
'RevertSnippetVersionRequest',
|
||||
json,
|
||||
($checkedConvert) {
|
||||
final val = RevertSnippetVersionRequest(
|
||||
snippetId: $checkedConvert('snippetId', (v) => v as String),
|
||||
version: $checkedConvert('version', (v) => (v as num).toInt()),
|
||||
changeDescription:
|
||||
$checkedConvert('changeDescription', (v) => v as String?),
|
||||
);
|
||||
return val;
|
||||
},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RevertSnippetVersionRequestToJson(
|
||||
RevertSnippetVersionRequest instance) {
|
||||
final val = <String, dynamic>{
|
||||
'snippetId': instance.snippetId,
|
||||
'version': instance.version,
|
||||
};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('changeDescription', instance.changeDescription);
|
||||
return val;
|
||||
}
|
||||
950
AINoval/lib/models/novel_structure.dart
Normal file
950
AINoval/lib/models/novel_structure.dart
Normal file
@@ -0,0 +1,950 @@
|
||||
import 'package:ainoval/utils/logger.dart';
|
||||
|
||||
/// 小说模型
|
||||
class Novel {
|
||||
Novel({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.coverUrl= '',
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.acts = const [],
|
||||
this.lastEditedChapterId,
|
||||
this.author,
|
||||
this.wordCount = 0,
|
||||
this.readTime = 0,
|
||||
this.version = 1,
|
||||
this.contributors = const <String>[],
|
||||
});
|
||||
|
||||
/// 从JSON创建Novel实例
|
||||
factory Novel.fromJson(Map<String, dynamic> json) {
|
||||
AppLogger.v(
|
||||
'NovelModel', 'Parsing Novel from JSON: ${json['id']}'); // 添加日志确认进入
|
||||
try {
|
||||
// --- 这是关键部分 ---
|
||||
List<Act> parsedActs = [];
|
||||
|
||||
// 处理acts数据 - 优先检查structure.acts路径
|
||||
if (json.containsKey('structure') && json['structure'] is Map) {
|
||||
final structure = json['structure'] as Map<String, dynamic>;
|
||||
|
||||
if (structure.containsKey('acts') && structure['acts'] is List) {
|
||||
AppLogger.v('NovelModel',
|
||||
'Found "structure.acts" list with ${(structure['acts'] as List).length} items.');
|
||||
|
||||
parsedActs = (structure['acts'] as List)
|
||||
.map((actJson) {
|
||||
if (actJson is Map<String, dynamic>) {
|
||||
// 对列表中的每个元素调用 Act.fromJson
|
||||
return Act.fromJson(actJson);
|
||||
} else {
|
||||
// 处理无效数据项
|
||||
AppLogger.w('NovelModel',
|
||||
'Invalid item in "structure.acts" list: $actJson');
|
||||
return null; // 返回null让whereType过滤掉
|
||||
}
|
||||
})
|
||||
.whereType<Act>() // 过滤掉可能的 null 值
|
||||
.toList();
|
||||
|
||||
AppLogger.v('NovelModel',
|
||||
'Successfully parsed ${parsedActs.length} acts from structure.acts.');
|
||||
} else {
|
||||
AppLogger.w('NovelModel',
|
||||
'"structure.acts" field is missing, null, or not a list in JSON for Novel ${json['id']}');
|
||||
}
|
||||
}
|
||||
// 如果在structure中没有找到有效的acts,尝试直接从json的acts字段读取
|
||||
else if (json.containsKey('acts') && json['acts'] is List) {
|
||||
AppLogger.v('NovelModel',
|
||||
'Found direct "acts" list with ${(json['acts'] as List).length} items.');
|
||||
|
||||
parsedActs = (json['acts'] as List)
|
||||
.map((actJson) {
|
||||
if (actJson is Map<String, dynamic>) {
|
||||
return Act.fromJson(actJson);
|
||||
} else {
|
||||
AppLogger.w('NovelModel',
|
||||
'Invalid item in direct "acts" list: $actJson');
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whereType<Act>()
|
||||
.toList();
|
||||
|
||||
AppLogger.v('NovelModel',
|
||||
'Successfully parsed ${parsedActs.length} acts from direct acts field.');
|
||||
} else {
|
||||
AppLogger.w('NovelModel',
|
||||
'No valid acts field found in JSON for Novel ${json['id']}');
|
||||
}
|
||||
// --- 关键部分结束 ---
|
||||
|
||||
// 解析元数据
|
||||
final metadata = json['metadata'] as Map<String, dynamic>? ?? {};
|
||||
final wordCount = metadata['wordCount'] is int ? metadata['wordCount'] as int : 0;
|
||||
final readTime = metadata['readTime'] is int ? metadata['readTime'] as int : 0;
|
||||
final version = metadata['version'] is int ? metadata['version'] as int : 1;
|
||||
|
||||
// 处理contributors列表
|
||||
List<String> contributors = [];
|
||||
if (metadata.containsKey('contributors') && metadata['contributors'] is List) {
|
||||
// 尝试转换每个元素为String
|
||||
for (var item in metadata['contributors'] as List) {
|
||||
if (item is String) {
|
||||
contributors.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析日期
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
|
||||
try {
|
||||
createdAt = json.containsKey('createdAt') && json['createdAt'] is String
|
||||
? DateTime.parse(json['createdAt'] as String)
|
||||
: DateTime.now();
|
||||
} catch (e) {
|
||||
AppLogger.w('NovelModel', '解析createdAt失败,使用当前时间', e);
|
||||
createdAt = DateTime.now();
|
||||
}
|
||||
|
||||
try {
|
||||
updatedAt = json.containsKey('updatedAt') && json['updatedAt'] is String
|
||||
? DateTime.parse(json['updatedAt'] as String)
|
||||
: DateTime.now();
|
||||
} catch (e) {
|
||||
AppLogger.w('NovelModel', '解析updatedAt失败,使用当前时间', e);
|
||||
updatedAt = DateTime.now();
|
||||
}
|
||||
|
||||
// 处理封面URL字段
|
||||
String coverUrl = '';
|
||||
if (json.containsKey('coverUrl') && json['coverUrl'] is String) {
|
||||
coverUrl = json['coverUrl'] as String;
|
||||
} else if (json.containsKey('coverImage') && json['coverImage'] is String) {
|
||||
// 兼容后端可能使用coverImage字段
|
||||
coverUrl = json['coverImage'] as String;
|
||||
}
|
||||
|
||||
// 创建Novel对象
|
||||
return Novel(
|
||||
id: json['id'] as String? ?? 'unknown_${DateTime.now().millisecondsSinceEpoch}',
|
||||
title: json['title'] as String? ?? '无标题',
|
||||
coverUrl: coverUrl,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
acts: parsedActs,
|
||||
lastEditedChapterId: json['lastEditedChapterId'] as String?,
|
||||
author: json['author'] != null
|
||||
? Author.fromJson(json['author'] as Map<String, dynamic>)
|
||||
: null,
|
||||
wordCount: wordCount,
|
||||
readTime: readTime,
|
||||
version: version,
|
||||
contributors: contributors,
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
AppLogger.e('NovelModel', 'Error parsing Novel from JSON: ${json['id']}',
|
||||
e, stackTrace);
|
||||
// 返回一个基本的空Novel对象,避免应用崩溃
|
||||
return Novel(
|
||||
id: json['id'] as String? ?? 'error_${DateTime.now().millisecondsSinceEpoch}',
|
||||
title: '解析错误 - ${json['title'] ?? '无标题'}',
|
||||
coverUrl: '',
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
acts: [],
|
||||
wordCount: 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
final String id;
|
||||
final String title;
|
||||
final String coverUrl;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final List<Act> acts;
|
||||
final String? lastEditedChapterId; // 上次编辑的章节ID
|
||||
final Author? author; // 作者信息
|
||||
final int wordCount; // 总字数(来自元数据)
|
||||
final int readTime; // 估计阅读时间(分钟)
|
||||
final int version; // 文档版本号
|
||||
final List<String> contributors; // 贡献者列表
|
||||
|
||||
/// 计算小说总字数(如果需要动态计算)
|
||||
int calculateWordCount() {
|
||||
int totalWordCount = 0;
|
||||
for (final act in acts) {
|
||||
for (final chapter in act.chapters) {
|
||||
for (final scene in chapter.scenes) {
|
||||
totalWordCount += scene.wordCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalWordCount;
|
||||
}
|
||||
|
||||
/// 计算小说总场景数(考虑 sceneIds 字段)
|
||||
int getSceneCount() {
|
||||
int totalSceneCount = 0;
|
||||
//AppLogger.d('Novel', '开始计算场景总数');
|
||||
|
||||
for (final act in acts) {
|
||||
int actSceneCount = 0;
|
||||
for (final chapter in act.chapters) {
|
||||
// 使用 sceneCount 属性,它会返回 scenes 和 sceneIds 中的较大值
|
||||
int chapterSceneCount = chapter.sceneCount;
|
||||
actSceneCount += chapterSceneCount;
|
||||
//AppLogger.d('Novel', '章节 ${chapter.id} 场景数: scenes=${chapter.scenes.length}, sceneIds=${chapter.sceneIds.length}, 取较大值=${chapterSceneCount}');
|
||||
}
|
||||
totalSceneCount += actSceneCount;
|
||||
//AppLogger.d('Novel', '卷 ${act.id} 场景总数: $actSceneCount');
|
||||
}
|
||||
|
||||
//AppLogger.d('Novel', '小说场景总数: $totalSceneCount');
|
||||
return totalSceneCount;
|
||||
}
|
||||
|
||||
/// 计算小说总章节数
|
||||
int getChapterCount() {
|
||||
int totalChapterCount = 0;
|
||||
for (final act in acts) {
|
||||
totalChapterCount += act.chapters.length;
|
||||
}
|
||||
return totalChapterCount;
|
||||
}
|
||||
|
||||
/// 计算小说总卷数
|
||||
int getActCount() {
|
||||
return acts.length;
|
||||
}
|
||||
|
||||
/// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'coverUrl': coverUrl,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
'acts': acts.map((act) => act.toJson()).toList(),
|
||||
'lastEditedChapterId': lastEditedChapterId,
|
||||
'author': author?.toJson(),
|
||||
'metadata': {
|
||||
'wordCount': wordCount,
|
||||
'readTime': readTime,
|
||||
'version': version,
|
||||
'contributors': contributors,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// 创建Novel的副本
|
||||
Novel copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? coverUrl,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
List<Act>? acts,
|
||||
String? lastEditedChapterId,
|
||||
Author? author,
|
||||
int? wordCount,
|
||||
int? readTime,
|
||||
int? version,
|
||||
List<String>? contributors,
|
||||
}) {
|
||||
return Novel(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
coverUrl: coverUrl?? this.coverUrl,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
acts: acts ?? this.acts,
|
||||
lastEditedChapterId: lastEditedChapterId ?? this.lastEditedChapterId,
|
||||
author: author ?? this.author,
|
||||
wordCount: wordCount ?? this.wordCount,
|
||||
readTime: readTime ?? this.readTime,
|
||||
version: version ?? this.version,
|
||||
contributors: contributors ?? this.contributors,
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建一个空的小说结构
|
||||
static Novel createEmpty(String id, String title) {
|
||||
final now = DateTime.now();
|
||||
return Novel(
|
||||
id: id,
|
||||
title: title,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
acts: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// 添加一个新的Act
|
||||
Novel addAct(String title) {
|
||||
final newAct = Act(
|
||||
id: 'act_${DateTime.now().millisecondsSinceEpoch}',
|
||||
title: title,
|
||||
order: acts.length + 1,
|
||||
chapters: [],
|
||||
);
|
||||
|
||||
return copyWith(
|
||||
acts: [...acts, newAct],
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取指定Act
|
||||
Act? getAct(String actId) {
|
||||
try {
|
||||
return acts.firstWhere((act) => act.id == actId);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取指定Chapter
|
||||
Chapter? getChapter(String actId, String chapterId) {
|
||||
final act = getAct(actId);
|
||||
if (act == null) return null;
|
||||
|
||||
try {
|
||||
return act.chapters.firstWhere((chapter) => chapter.id == chapterId);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据章节ID直接获取章节,不需要知道Act ID
|
||||
Chapter? getChapterById(String chapterId) {
|
||||
for (final act in acts) {
|
||||
try {
|
||||
final chapter =
|
||||
act.chapters.firstWhere((chapter) => chapter.id == chapterId);
|
||||
return chapter;
|
||||
} catch (e) {
|
||||
// 继续查找下一个act
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 获取指定Scene
|
||||
Scene? getScene(String actId, String chapterId, {String? sceneId}) {
|
||||
final chapter = getChapter(actId, chapterId);
|
||||
if (chapter == null) return null;
|
||||
|
||||
if (sceneId != null) {
|
||||
// 如果提供了sceneId,则获取特定Scene
|
||||
return chapter.getScene(sceneId);
|
||||
} else if (chapter.scenes.isNotEmpty) {
|
||||
// 否则返回第一个Scene
|
||||
return chapter.scenes.first;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 获取上下文章节(前后n章)
|
||||
List<Chapter> getContextChapters(String chapterId, int n) {
|
||||
// 提取所有章节
|
||||
List<Chapter> allChapters = [];
|
||||
for (final act in acts) {
|
||||
allChapters.addAll(act.chapters);
|
||||
}
|
||||
|
||||
// 按order排序
|
||||
allChapters.sort((a, b) => a.order.compareTo(b.order));
|
||||
|
||||
// 找到当前章节的索引
|
||||
int currentIndex =
|
||||
allChapters.indexWhere((chapter) => chapter.id == chapterId);
|
||||
if (currentIndex == -1) {
|
||||
// 如果找不到当前章节,返回前n章
|
||||
return allChapters.take(n).toList();
|
||||
}
|
||||
|
||||
// 计算前后n章的范围
|
||||
int startIndex = (currentIndex - n) < 0 ? 0 : (currentIndex - n);
|
||||
int endIndex = (currentIndex + n) >= allChapters.length
|
||||
? allChapters.length - 1
|
||||
: (currentIndex + n);
|
||||
|
||||
// 提取前后n章
|
||||
return allChapters.sublist(startIndex, endIndex + 1);
|
||||
}
|
||||
|
||||
/// 更新最后编辑的章节ID
|
||||
Novel updateLastEditedChapter(String chapterId) {
|
||||
return copyWith(
|
||||
lastEditedChapterId: chapterId,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 幕模型(如Act 1, Act 2等)
|
||||
class Act {
|
||||
Act({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.order,
|
||||
this.chapters = const [],
|
||||
});
|
||||
|
||||
/// 从JSON创建Act实例
|
||||
factory Act.fromJson(Map<String, dynamic> json) {
|
||||
List<Chapter> parsedChapters = [];
|
||||
if (json['chapters'] != null && json['chapters'] is List) {
|
||||
parsedChapters = (json['chapters'] as List<dynamic>)
|
||||
.map((chapterJson) =>
|
||||
Chapter.fromJson(chapterJson as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
return Act(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
order: json['order'] as int,
|
||||
chapters: parsedChapters, // 使用解析后的列表
|
||||
);
|
||||
}
|
||||
final String id;
|
||||
final String title;
|
||||
final int order;
|
||||
final List<Chapter> chapters;
|
||||
|
||||
/// 计算Act的总字数
|
||||
int get wordCount {
|
||||
return chapters.fold(0, (sum, chapter) => sum + chapter.wordCount);
|
||||
}
|
||||
|
||||
/// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'order': order,
|
||||
'chapters': chapters.map((chapter) => chapter.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 创建Act的副本
|
||||
Act copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
int? order,
|
||||
List<Chapter>? chapters,
|
||||
}) {
|
||||
return Act(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
order: order ?? this.order,
|
||||
chapters: chapters ?? this.chapters,
|
||||
);
|
||||
}
|
||||
|
||||
/// 添加一个新的Chapter
|
||||
Act addChapter(String title) {
|
||||
// 创建一个默认的Scene
|
||||
final defaultScene = Scene.createEmpty();
|
||||
|
||||
final newChapter = Chapter(
|
||||
id: 'chapter_${DateTime.now().millisecondsSinceEpoch}',
|
||||
title: title,
|
||||
order: chapters.length + 1,
|
||||
scenes: [defaultScene], // 包含一个默认的Scene
|
||||
);
|
||||
|
||||
return copyWith(
|
||||
chapters: [...chapters, newChapter],
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取指定Chapter
|
||||
Chapter? getChapter(String chapterId) {
|
||||
try {
|
||||
return chapters.firstWhere((chapter) => chapter.id == chapterId);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 章节模型
|
||||
class Chapter {
|
||||
Chapter({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.order,
|
||||
this.scenes = const [],
|
||||
this.sceneIds = const [], // 添加 sceneIds 字段
|
||||
});
|
||||
|
||||
/// 从JSON创建Chapter实例
|
||||
factory Chapter.fromJson(Map<String, dynamic> json) {
|
||||
List<Scene> parsedScenes = [];
|
||||
List<String> parsedSceneIds = [];
|
||||
|
||||
// 解析场景列表
|
||||
if (json['scenes'] != null && json['scenes'] is List) {
|
||||
parsedScenes = (json['scenes'] as List<dynamic>)
|
||||
.map((sceneJson) => Scene.fromJson(sceneJson as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// 解析场景ID列表
|
||||
if (json['sceneIds'] != null && json['sceneIds'] is List) {
|
||||
parsedSceneIds = (json['sceneIds'] as List<dynamic>)
|
||||
.map((id) => id.toString())
|
||||
.toList();
|
||||
}
|
||||
|
||||
return Chapter(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
order: json['order'] as int,
|
||||
scenes: parsedScenes,
|
||||
sceneIds: parsedSceneIds, // 保存场景ID列表
|
||||
);
|
||||
}
|
||||
final String id;
|
||||
final String title;
|
||||
final int order;
|
||||
final List<Scene> scenes;
|
||||
final List<String> sceneIds; // 保存从后端返回的场景ID列表
|
||||
|
||||
/// 计算章节的总字数
|
||||
int get wordCount {
|
||||
return scenes.fold(0, (sum, scene) => sum + scene.wordCount);
|
||||
}
|
||||
|
||||
/// 获取场景总数(scenes列表或sceneIds列表中的较大值)
|
||||
int get sceneCount {
|
||||
int scenesLength = scenes.length;
|
||||
int sceneIdsLength = sceneIds.length;
|
||||
int result = scenesLength > sceneIdsLength ? scenesLength : sceneIdsLength;
|
||||
//AppLogger.d('Chapter', '章节 $id 场景计数: scenes=$scenesLength, sceneIds=$sceneIdsLength, 取较大值=$result');
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'order': order,
|
||||
'scenes': scenes.map((scene) => scene.toJson()).toList(),
|
||||
'sceneIds': sceneIds, // 添加场景ID列表
|
||||
};
|
||||
}
|
||||
|
||||
/// 创建Chapter的副本
|
||||
Chapter copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
int? order,
|
||||
List<Scene>? scenes,
|
||||
List<String>? sceneIds, // 添加sceneIds参数
|
||||
}) {
|
||||
return Chapter(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
order: order ?? this.order,
|
||||
scenes: scenes ?? this.scenes,
|
||||
sceneIds: sceneIds ?? this.sceneIds, // 设置sceneIds
|
||||
);
|
||||
}
|
||||
|
||||
/// 添加一个新的Scene
|
||||
void addScene(Scene newScene) {
|
||||
scenes.add(newScene);
|
||||
}
|
||||
|
||||
/// 获取指定Scene
|
||||
Scene? getScene(String sceneId) {
|
||||
try {
|
||||
return scenes.firstWhere((scene) => scene.id == sceneId);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新指定Scene
|
||||
Chapter updateScene(String sceneId, Scene updatedScene) {
|
||||
final updatedScenes = scenes.map((scene) {
|
||||
if (scene.id == sceneId) {
|
||||
return updatedScene;
|
||||
}
|
||||
return scene;
|
||||
}).toList();
|
||||
|
||||
return copyWith(scenes: updatedScenes);
|
||||
}
|
||||
}
|
||||
|
||||
/// 场景模型
|
||||
class Scene {
|
||||
Scene({
|
||||
required this.id,
|
||||
required this.content,
|
||||
required this.wordCount,
|
||||
required this.summary,
|
||||
required this.lastEdited,
|
||||
this.title = '',
|
||||
this.actId = '',
|
||||
this.chapterId = '',
|
||||
this.version = 1,
|
||||
this.history = const [],
|
||||
});
|
||||
|
||||
/// 从JSON创建Scene实例
|
||||
factory Scene.fromJson(Map<String, dynamic> json) {
|
||||
// 创建安全的Summary对象
|
||||
Summary summaryObj;
|
||||
try {
|
||||
// 处理summary字段 - 可能是字符串(后端)或对象(前端)
|
||||
if (json.containsKey('summary')) {
|
||||
final summaryData = json['summary'];
|
||||
if (summaryData is Map<String, dynamic>) {
|
||||
// 如果是对象格式,直接解析
|
||||
summaryObj = Summary.fromJson(summaryData);
|
||||
} else if (summaryData is String) {
|
||||
// 如果是字符串格式(后端发送的),创建Summary对象
|
||||
final sceneId = json['id'] ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||
summaryObj = Summary(
|
||||
id: '${sceneId}_summary',
|
||||
content: summaryData,
|
||||
);
|
||||
} else {
|
||||
// 其他格式,创建默认Summary
|
||||
final sceneId = json['id'] ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||
summaryObj = Summary(
|
||||
id: '${sceneId}_summary',
|
||||
content: '',
|
||||
);
|
||||
AppLogger.w('Scene.fromJson', '场景 $sceneId 的摘要字段类型不支持: ${summaryData.runtimeType}');
|
||||
}
|
||||
} else {
|
||||
// 创建默认Summary
|
||||
final sceneId = json['id'] ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||
summaryObj = Summary(
|
||||
id: '${sceneId}_summary',
|
||||
content: '',
|
||||
);
|
||||
AppLogger.w('Scene.fromJson', '场景 $sceneId 缺少摘要字段,已创建默认摘要');
|
||||
}
|
||||
} catch (e) {
|
||||
// 处理任何异常,创建默认Summary
|
||||
final sceneId = json['id'] ?? DateTime.now().millisecondsSinceEpoch.toString();
|
||||
summaryObj = Summary(
|
||||
id: '${sceneId}_summary',
|
||||
content: '',
|
||||
);
|
||||
AppLogger.e('Scene.fromJson', '解析场景 $sceneId 的摘要时出错', e);
|
||||
}
|
||||
|
||||
// 安全解析lastEdited字段,支持多种日期格式
|
||||
DateTime lastEditedDate;
|
||||
try {
|
||||
if (json.containsKey('lastEdited') && json['lastEdited'] != null) {
|
||||
final lastEditedStr = json['lastEdited'].toString();
|
||||
lastEditedDate = _parseDateTime(lastEditedStr);
|
||||
} else if (json.containsKey('updatedAt') && json['updatedAt'] != null) {
|
||||
// 兼容后端可能使用updatedAt字段
|
||||
final updatedAtStr = json['updatedAt'].toString();
|
||||
lastEditedDate = _parseDateTime(updatedAtStr);
|
||||
} else {
|
||||
lastEditedDate = DateTime.now();
|
||||
AppLogger.w('Scene.fromJson', '场景 ${json['id']} 缺少时间字段,使用当前时间');
|
||||
}
|
||||
} catch (e) {
|
||||
lastEditedDate = DateTime.now();
|
||||
AppLogger.w('Scene.fromJson', '解析场景 ${json['id']} 的时间字段失败,使用当前时间', e);
|
||||
}
|
||||
|
||||
return Scene(
|
||||
id: json['id'] ?? DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
content: json['content'] ?? '',
|
||||
wordCount: json['wordCount'] ?? 0,
|
||||
summary: summaryObj,
|
||||
lastEdited: lastEditedDate,
|
||||
title: json['title'] ?? '',
|
||||
actId: json['actId'] ?? '',
|
||||
chapterId: json['chapterId'] ?? '',
|
||||
version: json['version'] ?? 1,
|
||||
history: [],
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String content;
|
||||
final int wordCount;
|
||||
final Summary summary;
|
||||
final DateTime lastEdited;
|
||||
final String title;
|
||||
final String actId;
|
||||
final String chapterId;
|
||||
final int version;
|
||||
final List<HistoryEntry> history;
|
||||
|
||||
/// 解析多种日期格式的工具方法
|
||||
static DateTime _parseDateTime(String dateTimeStr) {
|
||||
if (dateTimeStr.isEmpty) {
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试标准ISO格式
|
||||
return DateTime.parse(dateTimeStr);
|
||||
} catch (e1) {
|
||||
try {
|
||||
// 尝试处理带毫秒的格式 "yyyy-MM-dd'T'HH:mm:ss.SSS"
|
||||
if (dateTimeStr.contains('T') && dateTimeStr.contains('.')) {
|
||||
// 如果包含时区信息,先移除
|
||||
String cleanStr = dateTimeStr;
|
||||
if (cleanStr.endsWith('Z')) {
|
||||
cleanStr = cleanStr.substring(0, cleanStr.length - 1);
|
||||
}
|
||||
if (cleanStr.contains('+') || cleanStr.lastIndexOf('-') > 10) {
|
||||
// 移除时区偏移
|
||||
final timeZoneIndex = cleanStr.lastIndexOf('+') > cleanStr.lastIndexOf('-')
|
||||
? cleanStr.lastIndexOf('+')
|
||||
: cleanStr.lastIndexOf('-');
|
||||
if (timeZoneIndex > 10) {
|
||||
cleanStr = cleanStr.substring(0, timeZoneIndex);
|
||||
}
|
||||
}
|
||||
return DateTime.parse(cleanStr);
|
||||
}
|
||||
|
||||
// 尝试其他常见格式
|
||||
// 格式:yyyy-MM-dd HH:mm:ss
|
||||
if (dateTimeStr.contains(' ') && !dateTimeStr.contains('T')) {
|
||||
final parts = dateTimeStr.split(' ');
|
||||
if (parts.length == 2) {
|
||||
final datePart = parts[0];
|
||||
final timePart = parts[1];
|
||||
final isoStr = '${datePart}T$timePart';
|
||||
return DateTime.parse(isoStr);
|
||||
}
|
||||
}
|
||||
|
||||
throw e1; // 如果都失败了,抛出原始异常
|
||||
} catch (e2) {
|
||||
AppLogger.w('Scene._parseDateTime', '无法解析日期格式: $dateTimeStr,使用当前时间');
|
||||
return DateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'content': content,
|
||||
'wordCount': wordCount,
|
||||
'summary': summary.toJson(),
|
||||
'lastEdited': lastEdited.toIso8601String(),
|
||||
'title': title,
|
||||
'actId': actId,
|
||||
'chapterId': chapterId,
|
||||
'version': version,
|
||||
'history': history.map((entry) => entry.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// 创建Scene的副本
|
||||
Scene copyWith({
|
||||
String? id,
|
||||
String? content,
|
||||
int? wordCount,
|
||||
Summary? summary,
|
||||
DateTime? lastEdited,
|
||||
String? title,
|
||||
String? actId,
|
||||
String? chapterId,
|
||||
int? version,
|
||||
List<HistoryEntry>? history,
|
||||
}) {
|
||||
return Scene(
|
||||
id: id ?? this.id,
|
||||
content: content ?? this.content,
|
||||
wordCount: wordCount ?? this.wordCount,
|
||||
summary: summary ?? this.summary,
|
||||
lastEdited: lastEdited ?? this.lastEdited,
|
||||
title: title ?? this.title,
|
||||
actId: actId ?? this.actId,
|
||||
chapterId: chapterId ?? this.chapterId,
|
||||
version: version ?? this.version,
|
||||
history: history ?? this.history,
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建一个空的场景
|
||||
static Scene createEmpty() {
|
||||
const defaultContent = '{"ops":[{"insert":"\\n"}]}'; // <-- 确保是这个值
|
||||
final now = DateTime.now();
|
||||
return Scene(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
content: defaultContent,
|
||||
wordCount: 0,
|
||||
summary: Summary(
|
||||
id: '${DateTime.now().millisecondsSinceEpoch}_summary',
|
||||
content: '',
|
||||
),
|
||||
lastEdited: now,
|
||||
title: '',
|
||||
actId: '',
|
||||
chapterId: '',
|
||||
version: 1,
|
||||
history: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建一个默认的场景
|
||||
static Scene createDefault(String sceneIdBase) {
|
||||
// 使用正确Quill Delta格式包含ops对象的内容
|
||||
const defaultContent = '{"ops":[{"insert":"\\n"}]}';
|
||||
final now = DateTime.now();
|
||||
return Scene(
|
||||
id: sceneIdBase,
|
||||
content: defaultContent,
|
||||
wordCount: 0,
|
||||
summary: Summary(
|
||||
id: '${sceneIdBase}_summary',
|
||||
content: '',
|
||||
),
|
||||
lastEdited: now,
|
||||
title: '新场景',
|
||||
actId: '',
|
||||
chapterId: '',
|
||||
version: 1,
|
||||
history: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 摘要模型
|
||||
class Summary {
|
||||
Summary({
|
||||
required this.id,
|
||||
required this.content,
|
||||
});
|
||||
|
||||
/// 从JSON创建Summary实例
|
||||
factory Summary.fromJson(Map<String, dynamic> json) {
|
||||
return Summary(
|
||||
id: json['id'] as String,
|
||||
content: json['content'] as String,
|
||||
);
|
||||
}
|
||||
final String id;
|
||||
final String content;
|
||||
|
||||
/// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'content': content,
|
||||
};
|
||||
}
|
||||
|
||||
/// 创建Summary的副本
|
||||
Summary copyWith({
|
||||
String? id,
|
||||
String? content,
|
||||
}) {
|
||||
return Summary(
|
||||
id: id ?? this.id,
|
||||
content: content ?? this.content,
|
||||
);
|
||||
}
|
||||
|
||||
/// 创建一个空的摘要
|
||||
static Summary createEmpty() {
|
||||
return Summary(
|
||||
id: 'summary_${DateTime.now().millisecondsSinceEpoch}',
|
||||
content: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryEntry {
|
||||
HistoryEntry({
|
||||
this.content,
|
||||
required this.updatedAt,
|
||||
required this.updatedBy,
|
||||
required this.reason,
|
||||
});
|
||||
|
||||
factory HistoryEntry.fromJson(Map<String, dynamic> json) {
|
||||
DateTime updatedAt;
|
||||
try {
|
||||
updatedAt = DateTime.parse(json['updatedAt']);
|
||||
} catch (e) {
|
||||
updatedAt = DateTime.now();
|
||||
}
|
||||
|
||||
return HistoryEntry(
|
||||
content: json['content'],
|
||||
updatedAt: updatedAt,
|
||||
updatedBy: json['updatedBy'] ?? 'unknown',
|
||||
reason: json['reason'] ?? '',
|
||||
);
|
||||
}
|
||||
final String? content;
|
||||
final DateTime updatedAt;
|
||||
final String updatedBy;
|
||||
final String reason;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'content': content,
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
'updatedBy': updatedBy,
|
||||
'reason': reason,
|
||||
};
|
||||
}
|
||||
|
||||
/// 作者信息模型
|
||||
class Author {
|
||||
Author({
|
||||
required this.id,
|
||||
required this.username,
|
||||
});
|
||||
|
||||
/// 从JSON创建Author实例
|
||||
factory Author.fromJson(Map<String, dynamic> json) {
|
||||
return Author(
|
||||
id: json['id'] ?? '',
|
||||
username: json['username'] ?? '未知作者',
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String username;
|
||||
|
||||
/// 转换为JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'username': username,
|
||||
};
|
||||
}
|
||||
|
||||
/// 创建Author的副本
|
||||
Author copyWith({
|
||||
String? id,
|
||||
String? username,
|
||||
}) {
|
||||
return Author(
|
||||
id: id ?? this.id,
|
||||
username: username ?? this.username,
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user