Files
oai-swift/oAI/Models/Message.swift
T
runeandClaude Sonnet 5 bd686873c4 Update commercial licensing contact URL to oai.pm
Consolidates the mac.oai.pm subdomain references (introduced in the
PolyForm Noncommercial relicense) to the root oai.pm domain, across
the LICENSE file, README, and all Swift source file headers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:33:53 +02:00

170 lines
5.1 KiB
Swift

//
// Message.swift
// oAI
//
// Core message model for chat conversations
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of oAI.
//
// oAI is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling oAI or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://oai.pm>.
import Foundation
enum MessageRole: String, Codable {
case user
case assistant
case system
}
/// Detail for a single tool call within a system "🔧 Calling:" message.
/// Not persisted — in-memory only.
struct ToolCallDetail {
let name: String
let input: String // raw JSON string of arguments
var result: String? // raw JSON string of result (nil while pending)
}
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
// Tool call details (not persisted — in-memory only for expandable display)
var toolCalls: [ToolCallDetail]? = nil
// Reasoning/thinking content (not persisted — in-memory only)
var thinkingContent: String? = nil
nonisolated 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"
}
}
}