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>
170 lines
5.1 KiB
Swift
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"
|
|
}
|
|
}
|
|
}
|