import 'package:freezed_annotation/freezed_annotation.dart'; // Freezed applies JsonKey to constructor parameters which triggers // invalid_annotation_target; suppress it for this data model file. // ignore_for_file: invalid_annotation_target part 'chat_message.freezed.dart'; part 'chat_message.g.dart'; @freezed sealed class ChatMessage with _$ChatMessage { const factory ChatMessage({ required String id, required String role, // 'user', 'assistant', 'system' required String content, required DateTime timestamp, String? model, @Default(false) bool isStreaming, List? attachmentIds, List>? files, // For generated images Map? metadata, @Default([]) List statusHistory, @Default([]) List followUps, @Default([]) List codeExecutions, @JsonKey( name: 'sources', fromJson: _sourceRefsFromJson, toJson: _sourceRefsToJson, ) @Default([]) List sources, Map? usage, // Previous generated versions of this assistant message (OpenWebUI-style) @JsonKey(includeFromJson: false, includeToJson: false) @Default([]) List versions, }) = _ChatMessage; factory ChatMessage.fromJson(Map json) => _$ChatMessageFromJson(json); } @freezed abstract class ChatMessageVersion with _$ChatMessageVersion { const factory ChatMessageVersion({ required String id, required String content, required DateTime timestamp, String? model, List>? files, @JsonKey( name: 'sources', fromJson: _sourceRefsFromJson, toJson: _sourceRefsToJson, ) @Default([]) List sources, @Default([]) List followUps, @Default([]) List codeExecutions, Map? usage, }) = _ChatMessageVersion; factory ChatMessageVersion.fromJson(Map json) => _$ChatMessageVersionFromJson(json); } @freezed abstract class ChatStatusUpdate with _$ChatStatusUpdate { const factory ChatStatusUpdate({ String? action, String? description, @JsonKey(fromJson: _safeBool) bool? done, @JsonKey(fromJson: _safeBool) bool? hidden, @JsonKey(fromJson: _safeInt) int? count, String? query, @JsonKey(fromJson: _safeStringList, toJson: _stringListToJson) @Default([]) List queries, @JsonKey(fromJson: _safeStringList, toJson: _stringListToJson) @Default([]) List urls, @JsonKey(fromJson: _statusItemsFromJson, toJson: _statusItemsToJson) @Default([]) List items, @JsonKey( name: 'timestamp', fromJson: _timestampFromJson, toJson: _timestampToJson, ) DateTime? occurredAt, }) = _ChatStatusUpdate; factory ChatStatusUpdate.fromJson(Map json) => _$ChatStatusUpdateFromJson(json); } @freezed abstract class ChatStatusItem with _$ChatStatusItem { const factory ChatStatusItem({ String? title, String? link, String? snippet, Map? metadata, }) = _ChatStatusItem; factory ChatStatusItem.fromJson(Map json) => _$ChatStatusItemFromJson(json); } @freezed abstract class ChatCodeExecution with _$ChatCodeExecution { const factory ChatCodeExecution({ @JsonKey(fromJson: _requiredString) required String id, @JsonKey(fromJson: _nullableString) String? name, @JsonKey(fromJson: _nullableString) String? language, @JsonKey(fromJson: _nullableString) String? code, ChatCodeExecutionResult? result, Map? metadata, }) = _ChatCodeExecution; factory ChatCodeExecution.fromJson(Map json) => _$ChatCodeExecutionFromJson(json); } @freezed abstract class ChatCodeExecutionResult with _$ChatCodeExecutionResult { const factory ChatCodeExecutionResult({ String? output, String? error, @JsonKey(fromJson: _executionFilesFromJson, toJson: _executionFilesToJson) @Default([]) List files, Map? metadata, }) = _ChatCodeExecutionResult; factory ChatCodeExecutionResult.fromJson(Map json) => _$ChatCodeExecutionResultFromJson(json); } @freezed abstract class ChatExecutionFile with _$ChatExecutionFile { const factory ChatExecutionFile({ @JsonKey(fromJson: _nullableString) String? name, @JsonKey(fromJson: _nullableString) String? url, Map? metadata, }) = _ChatExecutionFile; factory ChatExecutionFile.fromJson(Map json) => _$ChatExecutionFileFromJson(json); } @freezed abstract class ChatSourceReference with _$ChatSourceReference { const factory ChatSourceReference({ @JsonKey(fromJson: _nullableString) String? id, @JsonKey(fromJson: _nullableString) String? title, @JsonKey(fromJson: _nullableString) String? url, @JsonKey(fromJson: _nullableString) String? snippet, @JsonKey(fromJson: _nullableString) String? type, Map? metadata, }) = _ChatSourceReference; factory ChatSourceReference.fromJson(Map json) => _$ChatSourceReferenceFromJson(json); } List _safeStringList(dynamic value) { if (value is List) { return value .whereType() .map((e) => e?.toString().trim() ?? '') .where((s) => s.isNotEmpty) .toList(growable: false); } if (value is String && value.isNotEmpty) { return [value]; } return const []; } /// Safely parse a boolean from various formats (bool, String, int). bool? _safeBool(dynamic value) { if (value == null) return null; if (value is bool) return value; if (value is String) { final lower = value.toLowerCase(); if (lower == 'true' || lower == '1') return true; if (lower == 'false' || lower == '0') return false; return null; } if (value is num) return value != 0; return null; } /// Safely parse an integer from various formats (int, double, String). int? _safeInt(dynamic value) { if (value == null) return null; if (value is int) return value; if (value is double) return value.toInt(); if (value is String) return int.tryParse(value); return null; } List _stringListToJson(List value) => List.from(value, growable: false); List _statusItemsFromJson(dynamic value) { if (value is List) { return value .whereType() .map((item) { try { // Convert Map to Map safely final Map itemMap = {}; item.forEach((key, v) { itemMap[key.toString()] = v; }); return ChatStatusItem.fromJson(itemMap); } catch (e) { // Skip invalid entries return null; } }) .where((item) => item != null) .cast() .toList(growable: false); } return const []; } List> _statusItemsToJson(List value) { return value.map((item) => item.toJson()).toList(growable: false); } List _executionFilesFromJson(dynamic value) { if (value is List) { return value .whereType() .map((item) { try { // Convert Map to Map safely final Map fileMap = {}; item.forEach((key, v) { fileMap[key.toString()] = v; }); return ChatExecutionFile.fromJson(fileMap); } catch (e) { // Skip invalid entries return null; } }) .where((item) => item != null) .cast() .toList(growable: false); } return const []; } List> _executionFilesToJson( List files, ) { return files.map((file) => file.toJson()).toList(growable: false); } List _sourceRefsFromJson(dynamic value) { if (value is List) { return value .whereType() .map((item) { try { // Convert Map to Map safely final Map refMap = {}; item.forEach((key, v) { refMap[key.toString()] = v; }); return ChatSourceReference.fromJson(refMap); } catch (e) { // Skip invalid entries return null; } }) .where((item) => item != null) .cast() .toList(growable: false); } return const []; } List> _sourceRefsToJson( List references, ) { return references.map((ref) => ref.toJson()).toList(growable: false); } DateTime? _timestampFromJson(dynamic value) { if (value == null) return null; if (value is DateTime) return value; if (value is int) { // Heuristics: treat seconds vs milliseconds final isSeconds = value < 1000000000000; final millis = isSeconds ? value * 1000 : value; return DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true).toLocal(); } if (value is double) { final millis = value < 1000000000 ? (value * 1000).toInt() : value.toInt(); return DateTime.fromMillisecondsSinceEpoch(millis, isUtc: true).toLocal(); } if (value is String && value.isNotEmpty) { return DateTime.tryParse(value)?.toLocal(); } return null; } String? _timestampToJson(DateTime? value) => value?.toIso8601String(); String _requiredString(dynamic value, {String fallback = ''}) { if (value == null) return fallback; final str = value.toString(); return str.isEmpty ? fallback : str; } String? _nullableString(dynamic value) { if (value == null) return null; final str = value.toString(); return str.isEmpty ? null : str; }