import 'package:uuid/uuid.dart'; enum MessageRole { user, assistant, system } enum MessageType { text, voice, image } enum MessageStatus { sending, sent, error } class Message { final String id; final MessageRole role; final MessageType type; final String content; final String? audioUri; final String? imageBase64; final String? mimeType; final int timestamp; final MessageStatus? status; final int? duration; const Message({ required this.id, required this.role, required this.type, required this.content, required this.timestamp, this.audioUri, this.imageBase64, this.mimeType, this.status, this.duration, }); factory Message.text({ required MessageRole role, required String content, MessageStatus? status, }) { return Message( id: const Uuid().v4(), role: role, type: MessageType.text, content: content, timestamp: DateTime.now().millisecondsSinceEpoch, status: status, ); } factory Message.voice({ required MessageRole role, required String audioUri, String content = '', int? duration, MessageStatus? status, }) { return Message( id: const Uuid().v4(), role: role, type: MessageType.voice, content: content, audioUri: audioUri, timestamp: DateTime.now().millisecondsSinceEpoch, duration: duration, status: status, ); } factory Message.image({ required MessageRole role, required String imageBase64, String content = '', String? mimeType, MessageStatus? status, }) { return Message( id: const Uuid().v4(), role: role, type: MessageType.image, content: content, imageBase64: imageBase64, mimeType: mimeType, timestamp: DateTime.now().millisecondsSinceEpoch, status: status, ); } Message copyWith({ String? content, String? audioUri, String? imageBase64, String? mimeType, MessageStatus? status, int? duration, }) { return Message( id: id, role: role, type: type, content: content ?? this.content, audioUri: audioUri ?? this.audioUri, imageBase64: imageBase64 ?? this.imageBase64, mimeType: mimeType ?? this.mimeType, timestamp: timestamp, status: status ?? this.status, duration: duration ?? this.duration, ); } Map toJson() { return { 'id': id, 'role': role.name, 'type': type.name, 'content': content, if (audioUri != null) 'audioUri': audioUri, if (imageBase64 != null) 'imageBase64': imageBase64, if (mimeType != null) 'mimeType': mimeType, 'timestamp': timestamp, if (status != null) 'status': status!.name, if (duration != null) 'duration': duration, }; } /// Lightweight JSON for persistence (strips base64 audio, keeps file paths and images). Map toJsonLight() { // Keep audioUri if it's a file path (starts with '/') — these are saved audio files. // Strip base64 audio data (large, temp) — those won't survive restart. final keepAudio = audioUri != null && audioUri!.startsWith('/'); return { 'id': id, 'role': role.name, 'type': type.name, 'content': content, if (keepAudio) 'audioUri': audioUri, 'timestamp': timestamp, if (status != null) 'status': status!.name, if (duration != null) 'duration': duration, // Keep imageBase64 — images are typically 50-200 KB and must survive restart. if (imageBase64 != null) 'imageBase64': imageBase64, if (mimeType != null) 'mimeType': mimeType, }; } factory Message.fromJson(Map json) { return Message( id: json['id'] as String, role: MessageRole.values.byName(json['role'] as String), type: MessageType.values.byName(json['type'] as String), content: json['content'] as String? ?? '', audioUri: json['audioUri'] as String?, imageBase64: json['imageBase64'] as String?, mimeType: json['mimeType'] as String?, timestamp: json['timestamp'] as int, status: json['status'] != null ? MessageStatus.values.byName(json['status'] as String) : null, duration: json['duration'] as int?, ); } /// Returns true if this is a voice message that has neither audio nor text. bool get isEmptyVoice => type == MessageType.voice && (audioUri == null || audioUri!.isEmpty) && content.isEmpty; /// Returns true if this is a text message with no content (empty bubble). bool get isEmptyText => type == MessageType.text && content.trim().isEmpty && imageBase64 == null; }