Added a lot of functionality. Bugfixes and changes
This commit is contained in:
@@ -13,19 +13,22 @@ struct Conversation: Identifiable, Codable {
|
||||
var messages: [Message]
|
||||
let createdAt: Date
|
||||
var updatedAt: Date
|
||||
|
||||
var primaryModel: String? // Primary model used in this conversation
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
name: String,
|
||||
messages: [Message] = [],
|
||||
createdAt: Date = Date(),
|
||||
updatedAt: Date = Date()
|
||||
updatedAt: Date = Date(),
|
||||
primaryModel: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.messages = messages
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
self.primaryModel = primaryModel
|
||||
}
|
||||
|
||||
var messageCount: Int {
|
||||
|
||||
88
oAI/Models/EmailLog.swift
Normal file
88
oAI/Models/EmailLog.swift
Normal file
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// EmailLog.swift
|
||||
// oAI
|
||||
//
|
||||
// Email processing log entry model
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum EmailLogStatus: String, Codable {
|
||||
case success
|
||||
case error
|
||||
}
|
||||
|
||||
struct EmailLog: Identifiable, Codable, Equatable {
|
||||
let id: UUID
|
||||
let timestamp: Date
|
||||
let sender: String
|
||||
let subject: String
|
||||
let emailContent: String // Preview of email body
|
||||
let aiResponse: String? // The AI's response (nil if error before response)
|
||||
let status: EmailLogStatus
|
||||
let errorMessage: String? // Error details if status == .error
|
||||
let tokens: Int?
|
||||
let cost: Double?
|
||||
let responseTime: TimeInterval? // Time to generate response in seconds
|
||||
let modelId: String? // Model that handled the email
|
||||
|
||||
init(
|
||||
id: UUID = UUID(),
|
||||
timestamp: Date = Date(),
|
||||
sender: String,
|
||||
subject: String,
|
||||
emailContent: String,
|
||||
aiResponse: String? = nil,
|
||||
status: EmailLogStatus,
|
||||
errorMessage: String? = nil,
|
||||
tokens: Int? = nil,
|
||||
cost: Double? = nil,
|
||||
responseTime: TimeInterval? = nil,
|
||||
modelId: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.timestamp = timestamp
|
||||
self.sender = sender
|
||||
self.subject = subject
|
||||
self.emailContent = emailContent
|
||||
self.aiResponse = aiResponse
|
||||
self.status = status
|
||||
self.errorMessage = errorMessage
|
||||
self.tokens = tokens
|
||||
self.cost = cost
|
||||
self.responseTime = responseTime
|
||||
self.modelId = modelId
|
||||
}
|
||||
|
||||
static func == (lhs: EmailLog, rhs: EmailLog) -> Bool {
|
||||
lhs.id == rhs.id &&
|
||||
lhs.sender == rhs.sender &&
|
||||
lhs.subject == rhs.subject &&
|
||||
lhs.status == rhs.status
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Display Helpers
|
||||
|
||||
extension EmailLogStatus {
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .success: return "Success"
|
||||
case .error: return "Error"
|
||||
}
|
||||
}
|
||||
|
||||
var iconName: String {
|
||||
switch self {
|
||||
case .success: return "checkmark.circle.fill"
|
||||
case .error: return "xmark.circle.fill"
|
||||
}
|
||||
}
|
||||
|
||||
var color: String {
|
||||
switch self {
|
||||
case .success: return "green"
|
||||
case .error: return "red"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ struct Message: Identifiable, Codable, Equatable {
|
||||
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
|
||||
@@ -40,6 +41,7 @@ struct Message: Identifiable, Codable, Equatable {
|
||||
attachments: [FileAttachment]? = nil,
|
||||
responseTime: TimeInterval? = nil,
|
||||
wasInterrupted: Bool = false,
|
||||
modelId: String? = nil,
|
||||
isStreaming: Bool = false,
|
||||
generatedImages: [Data]? = nil
|
||||
) {
|
||||
@@ -52,12 +54,13 @@ struct Message: Identifiable, Codable, Equatable {
|
||||
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
|
||||
case id, role, content, tokens, cost, timestamp, attachments, responseTime, wasInterrupted, modelId
|
||||
}
|
||||
|
||||
static func == (lhs: Message, rhs: Message) -> Bool {
|
||||
|
||||
@@ -20,7 +20,8 @@ struct Settings: Codable {
|
||||
var streamEnabled: Bool
|
||||
var maxTokens: Int
|
||||
var systemPrompt: String?
|
||||
|
||||
var customPromptMode: CustomPromptMode
|
||||
|
||||
// Feature flags
|
||||
var onlineMode: Bool
|
||||
var memoryEnabled: Bool
|
||||
@@ -58,7 +59,7 @@ struct Settings: Codable {
|
||||
case anthropicNative = "anthropic_native"
|
||||
case duckduckgo
|
||||
case google
|
||||
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .anthropicNative: return "Anthropic Native"
|
||||
@@ -67,6 +68,25 @@ struct Settings: Codable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum CustomPromptMode: String, Codable, CaseIterable {
|
||||
case append = "append"
|
||||
case replace = "replace"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .append: return "Append to Default"
|
||||
case .replace: return "Replace Default (BYOP)"
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .append: return "Your custom prompt will be added after the default system prompt"
|
||||
case .replace: return "Only use your custom prompt (Bring Your Own Prompt)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default settings
|
||||
static let `default` = Settings(
|
||||
@@ -79,6 +99,7 @@ struct Settings: Codable {
|
||||
streamEnabled: true,
|
||||
maxTokens: 4096,
|
||||
systemPrompt: nil,
|
||||
customPromptMode: .append,
|
||||
onlineMode: false,
|
||||
memoryEnabled: true,
|
||||
mcpEnabled: false,
|
||||
|
||||
257
oAI/Models/SyncModels.swift
Normal file
257
oAI/Models/SyncModels.swift
Normal file
@@ -0,0 +1,257 @@
|
||||
import Foundation
|
||||
|
||||
enum SyncAuthMethod: String, CaseIterable, Codable {
|
||||
case ssh = "ssh"
|
||||
case password = "password"
|
||||
case token = "token"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .ssh: return "SSH Key"
|
||||
case .password: return "Username + Password"
|
||||
case .token: return "Access Token"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SyncError: LocalizedError {
|
||||
case notConfigured
|
||||
case missingCredentials
|
||||
case gitNotFound
|
||||
case gitFailed(String)
|
||||
case repoNotCloned
|
||||
case secretsDetected([String])
|
||||
case parseError(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .notConfigured:
|
||||
return "Git sync not configured. Check Settings > Sync."
|
||||
case .missingCredentials:
|
||||
return "Missing credentials. Check authentication method in Settings > Sync."
|
||||
case .gitNotFound:
|
||||
return "Git not found. Install Xcode Command Line Tools."
|
||||
case .gitFailed(let message):
|
||||
return "Git command failed: \(message)"
|
||||
case .repoNotCloned:
|
||||
return "Repository not cloned. Click 'Clone Repository' first."
|
||||
case .secretsDetected(let secrets):
|
||||
return "Secrets detected in conversations: \(secrets.joined(separator: ", ")). Remove before syncing."
|
||||
case .parseError(let message):
|
||||
return "Failed to parse conversation: \(message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SyncStatus: Equatable {
|
||||
var lastSyncTime: Date?
|
||||
var uncommittedChanges: Int = 0
|
||||
var isCloned: Bool = false
|
||||
var currentBranch: String?
|
||||
var remoteStatus: String? // "up-to-date", "ahead 3", "behind 2", etc.
|
||||
}
|
||||
|
||||
struct ConversationExport {
|
||||
let id: String
|
||||
let name: String
|
||||
let createdAt: Date
|
||||
let updatedAt: Date
|
||||
let primaryModel: String? // Primary model used in conversation
|
||||
let messages: [MessageExport]
|
||||
|
||||
struct MessageExport {
|
||||
let role: String
|
||||
let content: String
|
||||
let timestamp: Date
|
||||
let tokens: Int?
|
||||
let cost: Double?
|
||||
let modelId: String? // Model that generated this message
|
||||
}
|
||||
|
||||
func toMarkdown() -> String {
|
||||
var md = "# \(name)\n\n"
|
||||
md += "**ID**: `\(id)`\n"
|
||||
md += "**Created**: \(ISO8601DateFormatter().string(from: createdAt))\n"
|
||||
md += "**Updated**: \(ISO8601DateFormatter().string(from: updatedAt))\n"
|
||||
|
||||
// Add primary model if available
|
||||
if let primaryModel = primaryModel {
|
||||
md += "**Primary Model**: \(primaryModel)\n"
|
||||
}
|
||||
|
||||
// Calculate all unique models used
|
||||
let uniqueModels = Set(messages.compactMap { $0.modelId })
|
||||
if !uniqueModels.isEmpty {
|
||||
md += "**Models Used**: \(uniqueModels.sorted().joined(separator: ", "))\n"
|
||||
}
|
||||
|
||||
md += "\n---\n\n"
|
||||
|
||||
for message in messages {
|
||||
md += "## \(message.role.capitalized)\n\n"
|
||||
md += message.content + "\n\n"
|
||||
|
||||
var meta: [String] = []
|
||||
if let modelId = message.modelId {
|
||||
meta.append("Model: \(modelId)")
|
||||
}
|
||||
if let tokens = message.tokens {
|
||||
meta.append("Tokens: \(tokens)")
|
||||
}
|
||||
if let cost = message.cost {
|
||||
meta.append(String(format: "Cost: $%.4f", cost))
|
||||
}
|
||||
if !meta.isEmpty {
|
||||
md += "*\(meta.joined(separator: " | "))*\n\n"
|
||||
}
|
||||
|
||||
md += "---\n\n"
|
||||
}
|
||||
|
||||
return md
|
||||
}
|
||||
|
||||
/// Parse markdown back to ConversationExport
|
||||
static func fromMarkdown(_ markdown: String) throws -> ConversationExport {
|
||||
let lines = markdown.components(separatedBy: .newlines)
|
||||
var lineIndex = 0
|
||||
|
||||
// Parse title (first line should be "# Title")
|
||||
guard lineIndex < lines.count, lines[lineIndex].hasPrefix("# ") else {
|
||||
throw SyncError.parseError("Missing title")
|
||||
}
|
||||
let name = String(lines[lineIndex].dropFirst(2)).trimmingCharacters(in: .whitespaces)
|
||||
lineIndex += 1
|
||||
|
||||
// Skip empty line
|
||||
while lineIndex < lines.count && lines[lineIndex].trimmingCharacters(in: .whitespaces).isEmpty {
|
||||
lineIndex += 1
|
||||
}
|
||||
|
||||
// Parse ID
|
||||
guard lineIndex < lines.count, lines[lineIndex].hasPrefix("**ID**: `") else {
|
||||
throw SyncError.parseError("Missing ID")
|
||||
}
|
||||
let idLine = lines[lineIndex]
|
||||
let idStart = idLine.index(idLine.startIndex, offsetBy: 9)
|
||||
let idEnd = idLine.lastIndex(of: "`") ?? idLine.endIndex
|
||||
let id = String(idLine[idStart..<idEnd])
|
||||
lineIndex += 1
|
||||
|
||||
// Parse Created date
|
||||
guard lineIndex < lines.count, lines[lineIndex].hasPrefix("**Created**: ") else {
|
||||
throw SyncError.parseError("Missing Created date")
|
||||
}
|
||||
let createdStr = String(lines[lineIndex].dropFirst(13))
|
||||
guard let createdAt = ISO8601DateFormatter().date(from: createdStr) else {
|
||||
throw SyncError.parseError("Invalid Created date format")
|
||||
}
|
||||
lineIndex += 1
|
||||
|
||||
// Parse Updated date
|
||||
guard lineIndex < lines.count, lines[lineIndex].hasPrefix("**Updated**: ") else {
|
||||
throw SyncError.parseError("Missing Updated date")
|
||||
}
|
||||
let updatedStr = String(lines[lineIndex].dropFirst(13))
|
||||
guard let updatedAt = ISO8601DateFormatter().date(from: updatedStr) else {
|
||||
throw SyncError.parseError("Invalid Updated date format")
|
||||
}
|
||||
lineIndex += 1
|
||||
|
||||
// Parse optional Primary Model
|
||||
var primaryModel: String? = nil
|
||||
if lineIndex < lines.count && lines[lineIndex].hasPrefix("**Primary Model**: ") {
|
||||
primaryModel = String(lines[lineIndex].dropFirst(19)).trimmingCharacters(in: .whitespaces)
|
||||
lineIndex += 1
|
||||
}
|
||||
|
||||
// Skip optional Models Used line
|
||||
if lineIndex < lines.count && lines[lineIndex].hasPrefix("**Models Used**: ") {
|
||||
lineIndex += 1
|
||||
}
|
||||
|
||||
// Skip to first message (past the ---)
|
||||
while lineIndex < lines.count && !lines[lineIndex].hasPrefix("## ") {
|
||||
lineIndex += 1
|
||||
}
|
||||
|
||||
// Parse messages
|
||||
var messages: [MessageExport] = []
|
||||
while lineIndex < lines.count {
|
||||
// Parse role (## User or ## Assistant)
|
||||
guard lines[lineIndex].hasPrefix("## ") else {
|
||||
lineIndex += 1
|
||||
continue
|
||||
}
|
||||
let role = String(lines[lineIndex].dropFirst(3)).lowercased().trimmingCharacters(in: .whitespaces)
|
||||
lineIndex += 1
|
||||
|
||||
// Skip empty line
|
||||
while lineIndex < lines.count && lines[lineIndex].trimmingCharacters(in: .whitespaces).isEmpty {
|
||||
lineIndex += 1
|
||||
}
|
||||
|
||||
// Parse content (until metadata line or ---)
|
||||
var content = ""
|
||||
while lineIndex < lines.count &&
|
||||
!lines[lineIndex].hasPrefix("*Tokens:") &&
|
||||
!lines[lineIndex].hasPrefix("---") &&
|
||||
!lines[lineIndex].hasPrefix("## ") {
|
||||
if !content.isEmpty {
|
||||
content += "\n"
|
||||
}
|
||||
content += lines[lineIndex]
|
||||
lineIndex += 1
|
||||
}
|
||||
content = content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// Parse metadata if present
|
||||
var modelId: String? = nil
|
||||
var tokens: Int? = nil
|
||||
var cost: Double? = nil
|
||||
if lineIndex < lines.count && lines[lineIndex].hasPrefix("*") {
|
||||
let metaLine = lines[lineIndex]
|
||||
// Extract modelId: "Model: gpt-4"
|
||||
if let modelMatch = metaLine.range(of: "Model: ([^|\\*]+)", options: .regularExpression) {
|
||||
let modelStr = String(metaLine[modelMatch]).dropFirst(7)
|
||||
modelId = modelStr.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
// Extract tokens: "Tokens: 50"
|
||||
if let tokensMatch = metaLine.range(of: "Tokens: (\\d+)", options: .regularExpression) {
|
||||
let tokensStr = String(metaLine[tokensMatch]).dropFirst(8)
|
||||
tokens = Int(tokensStr)
|
||||
}
|
||||
// Extract cost: "Cost: $0.0001"
|
||||
if let costMatch = metaLine.range(of: "Cost: \\$([0-9.]+)", options: .regularExpression) {
|
||||
let costStr = String(metaLine[costMatch]).dropFirst(7)
|
||||
cost = Double(costStr)
|
||||
}
|
||||
lineIndex += 1
|
||||
}
|
||||
|
||||
// Create message
|
||||
messages.append(MessageExport(
|
||||
role: role,
|
||||
content: content,
|
||||
timestamp: Date(), // Use current time as we don't store timestamps in markdown yet
|
||||
tokens: tokens,
|
||||
cost: cost,
|
||||
modelId: modelId
|
||||
))
|
||||
|
||||
// Skip to next message or end
|
||||
while lineIndex < lines.count && !lines[lineIndex].hasPrefix("## ") {
|
||||
lineIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
return ConversationExport(
|
||||
id: id,
|
||||
name: name,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
primaryModel: primaryModel,
|
||||
messages: messages
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user