马良AI写作初始化仓库
This commit is contained in:
459
AINoval/lib/models/scene_beat_data.dart
Normal file
459
AINoval/lib/models/scene_beat_data.dart
Normal file
@@ -0,0 +1,459 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user