// // Message.swift // oAI // // Core message model for chat conversations // // SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (C) 2026 Rune Olsen // // This file is part of oAI. // // oAI is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // oAI is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General // Public License for more details. // // You should have received a copy of the GNU Affero General Public // License along with oAI. If not, see . 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]? var responseTime: TimeInterval? // Time taken to generate response in seconds var wasInterrupted: Bool = false // Whether generation was cancelled var modelId: String? // Model ID that generated this message (e.g., "gpt-4", "claude-3-sonnet") // Streaming state (not persisted) var isStreaming: Bool = false // Star state (not persisted in messages table — tracked via message_metadata for saved conversations) var isStarred: 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, responseTime: TimeInterval? = nil, wasInterrupted: Bool = false, modelId: String? = 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.responseTime = responseTime self.wasInterrupted = wasInterrupted self.modelId = modelId self.isStreaming = isStreaming self.generatedImages = generatedImages } enum CodingKeys: String, CodingKey { case id, role, content, tokens, cost, timestamp, attachments, responseTime, wasInterrupted, modelId } static func == (lhs: Message, rhs: Message) -> Bool { lhs.id == rhs.id && lhs.content == rhs.content && lhs.tokens == rhs.tokens && lhs.cost == rhs.cost && lhs.responseTime == rhs.responseTime && lhs.wasInterrupted == rhs.wasInterrupted && 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" } } }