Added a lot of functionality. Bugfixes and changes
This commit is contained in:
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