Files
MaliangAINovalWriter/AINoval/lib/models/scene_beat_data.dart
2025-09-10 00:07:52 +08:00

459 lines
15 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import 'package:ainoval/models/ai_request_models.dart';
import 'package:ainoval/models/context_selection_models.dart';
import 'package:ainoval/utils/logger.dart';
/// 场景节拍组件数据模型
/// 存储在Quill文档中的自包含配置数据
class SceneBeatData {
/// AI请求的完整配置序列化为JSON字符串
/// 这是配置的"快照",包含模型、参数、上下文等所有信息
final String requestData;
/// AI最后生成的内容存储为Quill的Delta JSON字符串
/// 以便在内部的子编辑器中显示富文本
final String generatedContentDelta;
/// (可选) 为了UI方便记录上次加载的预设ID
/// 这样在下次打开编辑弹窗时,可以高亮显示对应的预设
/// **注意此字段仅用于UI展示不参与AI请求逻辑**
final String? lastUsedPresetId;
/// 🚀 新增选中的统一模型ID用于UI状态恢复
final String? selectedUnifiedModelId;
/// 🚀 新增:选中的字数长度('200', '400', '600' 或自定义值)
final String? selectedLength;
/// 🚀 新增温度参数0.0-2.0
final double temperature;
/// 🚀 新增Top-P参数0.0-1.0
final double topP;
/// 🚀 新增:是否启用智能上下文
final bool enableSmartContext;
/// 🚀 新增选中的提示词模板ID
final String? selectedPromptTemplateId;
/// 🚀 新增上下文选择数据序列化为JSON字符串
final String? contextSelectionsData;
/// 组件创建时间
final DateTime createdAt;
/// 组件最后更新时间
final DateTime updatedAt;
/// 组件状态
final SceneBeatStatus status;
/// 生成进度0.0-1.0
final double progress;
SceneBeatData({
required this.requestData,
this.generatedContentDelta = '[{"insert":"\\n"}]', // 默认为空文档
this.lastUsedPresetId,
this.selectedUnifiedModelId,
this.selectedLength,
this.temperature = 0.7,
this.topP = 0.9,
this.enableSmartContext = true,
this.selectedPromptTemplateId,
this.contextSelectionsData,
DateTime? createdAt,
DateTime? updatedAt,
this.status = SceneBeatStatus.draft,
this.progress = 0.0,
}) : createdAt = createdAt ?? DateTime.now(),
updatedAt = updatedAt ?? DateTime.now();
/// 从存储在Quill Delta中的JSON字符串反序列化
factory SceneBeatData.fromJson(String jsonString) {
try {
final map = jsonDecode(jsonString);
return SceneBeatData(
requestData: map['requestData'] as String? ?? '{}',
generatedContentDelta: map['generatedContentDelta'] as String? ?? '[{"insert":"\\n"}]',
lastUsedPresetId: map['lastUsedPresetId'] as String?,
selectedUnifiedModelId: map['selectedUnifiedModelId'] as String?,
selectedLength: map['selectedLength'] as String?,
temperature: (map['temperature'] as num? ?? 0.7).toDouble(),
topP: (map['topP'] as num? ?? 0.9).toDouble(),
enableSmartContext: map['enableSmartContext'] as bool? ?? true,
selectedPromptTemplateId: map['selectedPromptTemplateId'] as String?,
contextSelectionsData: map['contextSelectionsData'] as String?,
createdAt: map['createdAt'] != null
? DateTime.parse(map['createdAt'] as String)
: DateTime.now(),
updatedAt: map['updatedAt'] != null
? DateTime.parse(map['updatedAt'] as String)
: DateTime.now(),
status: SceneBeatStatus.values.firstWhere(
(s) => s.name == (map['status'] as String? ?? 'draft'),
orElse: () => SceneBeatStatus.draft,
),
progress: (map['progress'] as num? ?? 0.0).toDouble(),
);
} catch (e) {
AppLogger.e('SceneBeatData', '解析SceneBeatData失败: $e');
// 如果解析失败,返回一个安全的默认值
return SceneBeatData(
requestData: '{}',
generatedContentDelta: '[{"insert":"\\n"}]',
);
}
}
/// 序列化为JSON字符串以存储在Quill Delta中
String toJson() {
return jsonEncode({
'requestData': requestData,
'generatedContentDelta': generatedContentDelta,
'lastUsedPresetId': lastUsedPresetId,
'selectedUnifiedModelId': selectedUnifiedModelId,
'selectedLength': selectedLength,
'temperature': temperature,
'topP': topP,
'enableSmartContext': enableSmartContext,
'selectedPromptTemplateId': selectedPromptTemplateId,
'contextSelectionsData': contextSelectionsData,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'status': status.name,
'progress': progress,
});
}
/// 一个方便的getter用于获取反序列化后的请求对象
UniversalAIRequest? get parsedRequest {
try {
if (requestData.isEmpty || requestData == '{}') {
return null;
}
final requestJson = jsonDecode(requestData);
// 🚀 兼容性处理:将旧的 NOVEL_GENERATION 类型转换为 SCENE_BEAT_GENERATION
if (requestJson['requestType'] == 'NOVEL_GENERATION' &&
requestJson['metadata'] != null &&
requestJson['metadata']['action'] == 'scene_beat') {
requestJson['requestType'] = 'SCENE_BEAT_GENERATION';
AppLogger.d('SceneBeatData', '自动将旧版场景节拍请求类型更新为 SCENE_BEAT_GENERATION');
}
return UniversalAIRequest.fromJson(requestJson);
} catch (e) {
AppLogger.e('SceneBeatData', '解析UniversalAIRequest失败: $e');
return null;
}
}
/// 更新请求数据
SceneBeatData updateRequestData(UniversalAIRequest request) {
return SceneBeatData(
requestData: jsonEncode(request.toApiJson()),
generatedContentDelta: generatedContentDelta,
lastUsedPresetId: lastUsedPresetId,
selectedUnifiedModelId: selectedUnifiedModelId,
selectedLength: selectedLength,
temperature: temperature,
topP: topP,
enableSmartContext: enableSmartContext,
selectedPromptTemplateId: selectedPromptTemplateId,
contextSelectionsData: contextSelectionsData,
createdAt: createdAt,
updatedAt: DateTime.now(),
status: status,
progress: progress,
);
}
/// 更新生成的内容
SceneBeatData updateGeneratedContent(String deltaJson) {
return SceneBeatData(
requestData: requestData,
generatedContentDelta: deltaJson,
lastUsedPresetId: lastUsedPresetId,
selectedUnifiedModelId: selectedUnifiedModelId,
selectedLength: selectedLength,
temperature: temperature,
topP: topP,
enableSmartContext: enableSmartContext,
selectedPromptTemplateId: selectedPromptTemplateId,
contextSelectionsData: contextSelectionsData,
createdAt: createdAt,
updatedAt: DateTime.now(),
status: status == SceneBeatStatus.draft ? SceneBeatStatus.generated : status,
progress: progress,
);
}
/// 更新状态和进度
SceneBeatData updateStatus(SceneBeatStatus newStatus, {double? newProgress}) {
return SceneBeatData(
requestData: requestData,
generatedContentDelta: generatedContentDelta,
lastUsedPresetId: lastUsedPresetId,
selectedUnifiedModelId: selectedUnifiedModelId,
selectedLength: selectedLength,
temperature: temperature,
topP: topP,
enableSmartContext: enableSmartContext,
selectedPromptTemplateId: selectedPromptTemplateId,
contextSelectionsData: contextSelectionsData,
createdAt: createdAt,
updatedAt: DateTime.now(),
status: newStatus,
progress: newProgress ?? progress,
);
}
/// 复制数据
SceneBeatData copyWith({
String? requestData,
String? generatedContentDelta,
String? lastUsedPresetId,
String? selectedUnifiedModelId,
String? selectedLength,
double? temperature,
double? topP,
bool? enableSmartContext,
String? selectedPromptTemplateId,
String? contextSelectionsData,
DateTime? createdAt,
DateTime? updatedAt,
SceneBeatStatus? status,
double? progress,
}) {
return SceneBeatData(
requestData: requestData ?? this.requestData,
generatedContentDelta: generatedContentDelta ?? this.generatedContentDelta,
lastUsedPresetId: lastUsedPresetId ?? this.lastUsedPresetId,
selectedUnifiedModelId: selectedUnifiedModelId ?? this.selectedUnifiedModelId,
selectedLength: selectedLength ?? this.selectedLength,
temperature: temperature ?? this.temperature,
topP: topP ?? this.topP,
enableSmartContext: enableSmartContext ?? this.enableSmartContext,
selectedPromptTemplateId: selectedPromptTemplateId ?? this.selectedPromptTemplateId,
contextSelectionsData: contextSelectionsData ?? this.contextSelectionsData,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
status: status ?? this.status,
progress: progress ?? this.progress,
);
}
/// 创建默认的场景节拍数据
factory SceneBeatData.createDefault({
required String userId,
required String novelId,
String? initialPrompt,
}) {
// 创建默认的AI请求配置
final defaultRequest = UniversalAIRequest(
requestType: AIRequestType.sceneBeat,
userId: userId,
novelId: novelId,
prompt: initialPrompt ?? '续写故事。',
instructions: '一个关键时刻,重要的事情发生改变,推动故事发展。',
enableSmartContext: true,
parameters: {
'length': '400',
'temperature': 0.7,
'topP': 0.9,
'maxTokens': 4000,
},
metadata: {
'action': 'scene_beat',
'source': 'scene_beat_component',
'featureType': 'SCENE_BEAT_GENERATION',
},
);
return SceneBeatData(
requestData: jsonEncode(defaultRequest.toApiJson()),
generatedContentDelta: '[{"insert":"\\n"}]',
selectedLength: '400',
temperature: 0.7,
topP: 0.9,
enableSmartContext: true,
status: SceneBeatStatus.draft,
progress: 0.0,
);
}
/// 🚀 新增:获取解析后的上下文选择数据
ContextSelectionData? get parsedContextSelections {
if (contextSelectionsData == null || contextSelectionsData!.isEmpty) {
return null;
}
try {
final map = jsonDecode(contextSelectionsData!);
final selectedItems = <String, ContextSelectionItem>{};
final availableItems = <ContextSelectionItem>[];
final flatItems = <String, ContextSelectionItem>{};
// 解析选中的项目
final selectedList = map['selectedItems'] as List<dynamic>? ?? [];
for (final itemData in selectedList) {
final item = ContextSelectionItem(
id: itemData['id'] as String,
title: itemData['title'] as String,
type: ContextSelectionType.values.firstWhere(
(type) => type.value == itemData['type'], // 🚀 修复使用API值而不是displayName
orElse: () => ContextSelectionType.fullNovelText,
),
metadata: Map<String, dynamic>.from(itemData['metadata'] ?? {}),
selectionState: SelectionState.fullySelected,
);
selectedItems[item.id] = item;
availableItems.add(item);
flatItems[item.id] = item;
}
return ContextSelectionData(
novelId: map['novelId'] as String? ?? 'scene_beat',
selectedItems: selectedItems,
availableItems: availableItems,
flatItems: flatItems,
);
} catch (e) {
AppLogger.e('SceneBeatData', '解析上下文选择数据失败: $e');
return null;
}
}
/// 🚀 新增:更新上下文选择数据
SceneBeatData updateContextSelections(ContextSelectionData? contextData) {
String? serializedData;
if (contextData != null && contextData.selectedCount > 0) {
// 序列化选中的项目
final selectedList = contextData.selectedItems.values.map((item) => {
'id': item.id,
'title': item.title,
'type': item.type.value, // 🚀 修复使用API值而不是displayName
'metadata': item.metadata,
}).toList();
serializedData = jsonEncode({
'novelId': contextData.novelId,
'selectedItems': selectedList,
});
}
return copyWith(
contextSelectionsData: serializedData,
updatedAt: DateTime.now(),
);
}
/// 🚀 新增更新UI配置不更新请求数据
SceneBeatData updateUIConfig({
String? selectedUnifiedModelId,
String? selectedLength,
double? temperature,
double? topP,
bool? enableSmartContext,
String? selectedPromptTemplateId,
ContextSelectionData? contextSelections,
}) {
String? serializedContextData = this.contextSelectionsData;
if (contextSelections != null) {
final selectedList = contextSelections.selectedItems.values.map((item) => {
'id': item.id,
'title': item.title,
'type': item.type.value, // 🚀 修复使用API值而不是displayName
'metadata': item.metadata,
}).toList();
serializedContextData = jsonEncode({
'novelId': contextSelections.novelId,
'selectedItems': selectedList,
});
}
return copyWith(
selectedUnifiedModelId: selectedUnifiedModelId,
selectedLength: selectedLength,
temperature: temperature,
topP: topP,
enableSmartContext: enableSmartContext,
selectedPromptTemplateId: selectedPromptTemplateId,
contextSelectionsData: serializedContextData,
updatedAt: DateTime.now(),
);
}
/// 轻量级占位实例:折叠状态下仅存最小信息、避免占用大量内存
/// 注意:当面板真正展开时请调用 `createDefault` 或相应的 update* 方法替换掉该实例
static SceneBeatData get empty => SceneBeatData(requestData: '{}');
}
/// 场景节拍状态枚举
enum SceneBeatStatus {
/// 草稿状态 - 刚创建,还未生成内容
draft,
/// 生成中 - 正在进行AI生成
generating,
/// 已生成 - AI生成完成
generated,
/// 已应用 - 生成的内容已被用户接受并应用
applied,
/// 错误状态 - 生成过程中发生错误
error,
}
extension SceneBeatStatusExtension on SceneBeatStatus {
/// 获取状态的显示名称
String get displayName {
switch (this) {
case SceneBeatStatus.draft:
return '草稿';
case SceneBeatStatus.generating:
return '生成中';
case SceneBeatStatus.generated:
return '已生成';
case SceneBeatStatus.applied:
return '已应用';
case SceneBeatStatus.error:
return '错误';
}
}
/// 获取状态的图标
String get icon {
switch (this) {
case SceneBeatStatus.draft:
return '📝';
case SceneBeatStatus.generating:
return '';
case SceneBeatStatus.generated:
return '';
case SceneBeatStatus.applied:
return '🎯';
case SceneBeatStatus.error:
return '';
}
}
/// 是否可以编辑
bool get canEdit {
return this != SceneBeatStatus.generating;
}
/// 是否可以生成
bool get canGenerate {
return this != SceneBeatStatus.generating;
}
/// 是否可以应用
bool get canApply {
return this == SceneBeatStatus.generated;
}
}