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

457 lines
12 KiB
Dart
Raw Permalink 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 '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 }