// // Message.swift // oAI // // Core message model for chat conversations // import Foundation enum MessageRole: String, Codable { case user case assistant case system } struct Message: Identifiable, Codable, Equatable { let id: UUID let role: MessageRole var content: String var tokens: Int? var cost: Double? let timestamp: Date let attachments: [FileAttachment]? // Streaming state (not persisted) var isStreaming: Bool = false // Generated images from image-output models (base64-decoded PNG/JPEG data) var generatedImages: [Data]? = nil init( id: UUID = UUID(), role: MessageRole, content: String, tokens: Int? = nil, cost: Double? = nil, timestamp: Date = Date(), attachments: [FileAttachment]? = nil, isStreaming: Bool = false, generatedImages: [Data]? = nil ) { self.id = id self.role = role self.content = content self.tokens = tokens self.cost = cost self.timestamp = timestamp self.attachments = attachments self.isStreaming = isStreaming self.generatedImages = generatedImages } enum CodingKeys: String, CodingKey { case id, role, content, tokens, cost, timestamp, attachments } static func == (lhs: Message, rhs: Message) -> Bool { lhs.id == rhs.id && lhs.content == rhs.content && lhs.tokens == rhs.tokens && lhs.cost == rhs.cost && lhs.isStreaming == rhs.isStreaming && lhs.generatedImages == rhs.generatedImages } } struct FileAttachment: Codable, Equatable { let path: String let type: AttachmentType let data: Data? // file contents: raw bytes for images/PDFs, UTF-8 for text enum AttachmentType: String, Codable { case image case pdf case text } /// Detect attachment type from file extension static func typeFromExtension(_ path: String) -> AttachmentType { let ext = (path as NSString).pathExtension.lowercased() switch ext { case "png", "jpg", "jpeg", "gif", "webp", "bmp", "svg": return .image case "pdf": return .pdf default: return .text } } /// MIME type string for the file (used in base64 data URLs) var mimeType: String { let ext = (path as NSString).pathExtension.lowercased() switch ext { case "png": return "image/png" case "jpg", "jpeg": return "image/jpeg" case "gif": return "image/gif" case "webp": return "image/webp" case "bmp": return "image/bmp" case "svg": return "image/svg+xml" case "pdf": return "application/pdf" default: return "text/plain" } } } // MARK: - Display Helpers extension MessageRole { var displayName: String { switch self { case .user: return "You" case .assistant: return "Assistant" case .system: return "System" } } var iconName: String { switch self { case .user: return "person.circle.fill" case .assistant: return "cpu" case .system: return "info.circle.fill" } } }