enum MessageRole { user, assistant } class Message { final int? id; final int conversationId; final MessageRole role; final String content; final String status; // "complete" | "generating" final DateTime? createdAt; final Map? metadata; // Tool invocations attached to this message. Each entry matches the shape // persisted by the backend (`function`, `arguments`, `result`, `status`) so // the UI can render the same chips whether they arrive live over SSE or // from a reload. final List>? toolCalls; const Message({ this.id, required this.conversationId, required this.role, required this.content, this.status = 'complete', this.createdAt, this.metadata, this.toolCalls, }); factory Message.fromJson(Map json) { final rawCalls = json['tool_calls']; List>? parsedCalls; if (rawCalls is List) { parsedCalls = [ for (final tc in rawCalls) if (tc is Map) tc, ]; if (parsedCalls.isEmpty) parsedCalls = null; } return Message( id: json['id'] as int?, conversationId: json['conversation_id'] as int, role: json['role'] == 'user' ? MessageRole.user : MessageRole.assistant, content: json['content'] as String, status: json['status'] as String? ?? 'complete', createdAt: json['created_at'] != null ? DateTime.parse(json['created_at'] as String) : null, metadata: json['metadata'] as Map?, toolCalls: parsedCalls, ); } Message copyWith({ String? content, String? status, List>? toolCalls, }) => Message( id: id, conversationId: conversationId, role: role, content: content ?? this.content, status: status ?? this.status, createdAt: createdAt, metadata: metadata, toolCalls: toolCalls ?? this.toolCalls, ); }