// // ConversationExporter.swift // Zyquo Cloud // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Conversation export: Markdown text and PDF (rendered via NSPrintOperation- // compatible attributed text into a PDF context). // import AppKit import UniformTypeIdentifiers enum ConversationExporter { // MARK: - Markdown static func markdown(for conversation: Conversation) -> String { var lines: [String] = [] lines.append("# \(conversation.title)") lines.append("") lines.append("*Exported from Zyquo Cloud — \(conversation.provider.displayName) · \(conversation.modelID)*") if let system = conversation.systemPrompt, !system.isEmpty { lines.append("") lines.append("> **System:** \(system)") } for message in conversation.messages { lines.append("") switch message.role { case .user: lines.append("## 🧑 You") case .assistant: let model = message.modelID ?? conversation.modelID lines.append("## ☁️ Assistant (\(model))") case .system: lines.append("## System") } lines.append("") if let reasoning = message.reasoning, !reasoning.isEmpty { lines.append("
Thinking") lines.append("") lines.append(reasoning) lines.append("") lines.append("
") lines.append("") } lines.append(message.text) if !message.citations.isEmpty { lines.append("") lines.append("**Sources:**") for citation in message.citations { lines.append("\(citation.index). [\(citation.title ?? citation.url.absoluteString)](\(citation.url.absoluteString))") } } } lines.append("") return lines.joined(separator: "\n") } // MARK: - PDF static func pdfData(for conversation: Conversation) -> Data? { let text = markdown(for: conversation) let attributed = NSMutableAttributedString( string: text, attributes: [ .font: NSFont.systemFont(ofSize: 11), .foregroundColor: NSColor.textColor, ] ) let pageRect = CGRect(x: 0, y: 0, width: 612, height: 792) // US Letter let inset: CGFloat = 48 let data = NSMutableData() guard let consumer = CGDataConsumer(data: data as CFMutableData), var mediaBox = Optional(pageRect), let context = CGContext(consumer: consumer, mediaBox: &mediaBox, nil) else { return nil } let framesetter = CTFramesetterCreateWithAttributedString(attributed) var location = 0 while location < attributed.length { context.beginPDFPage(nil) let path = CGPath( rect: pageRect.insetBy(dx: inset, dy: inset), transform: nil ) let frame = CTFramesetterCreateFrame( framesetter, CFRange(location: location, length: 0), path, nil ) CTFrameDraw(frame, context) let visible = CTFrameGetVisibleStringRange(frame) location += max(visible.length, 1) context.endPDFPage() } context.closePDF() return data as Data } // MARK: - Save panel @MainActor static func presentSavePanel(for conversation: Conversation) { let panel = NSSavePanel() panel.allowedContentTypes = [ UTType(filenameExtension: "md") ?? .plainText, .pdf, ] panel.nameFieldStringValue = conversation.title .replacingOccurrences(of: "/", with: "-") panel.title = "Export Conversation" panel.begin { response in guard response == .OK, let url = panel.url else { return } if url.pathExtension.lowercased() == "pdf" { if let data = pdfData(for: conversation) { try? data.write(to: url) } } else { let text = markdown(for: conversation) try? text.data(using: .utf8)?.write(to: url) } } } }