Add HTML and PDF conversation export
Top item on the roadmap ranking from 2026-07-27 — multi-modal export alongside the existing Markdown/JSON. New ConversationExportService consolidates the two previously-duplicated Markdown builders (ChatViewModel and ConversationListView had separate copies of the same **User**/**Assistant** + --- format) and adds: - A hand-rolled Markdown->HTML renderer scoped to what actually shows up in chat messages (headers, bold/italic, inline code, fenced code blocks, lists, blockquotes, links, horizontal rules) rather than full CommonMark/GFM — no existing markdown-to-HTML utility existed in the codebase, and swift-markdown-ui is SwiftUI-view-only with no HTML-string export API. Content is HTML-escaped before any markdown substitution so example code containing "<div>" etc renders as visible text, not live markup. - PDF via an offscreen WKWebView loading that same HTML and calling the official createPDF(configuration:) API (macOS 11+) — no new project/framework linkage needed, WebKit is a system framework. Wired into every place Markdown export already existed: File menu (Export as HTML.../PDF...), /export slash command (now md|html|pdf|json), and a new Export submenu (Markdown/HTML/PDF) on each conversation row's context menu in the advanced conversation list, replacing the old single-format swipe-only export. Help docs and InputBar autocomplete updated to match.
This commit is contained in:
@@ -727,7 +727,7 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
let filename = args.count >= 2 ? args[1] : "conversation.\(format)"
|
||||
exportConversation(format: format, filename: filename)
|
||||
} else {
|
||||
showSystemMessage("Usage: /export md|json <filename>")
|
||||
showSystemMessage("Usage: /export md|html|pdf|json <filename>")
|
||||
}
|
||||
|
||||
case "/info":
|
||||
@@ -1769,13 +1769,29 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
return
|
||||
}
|
||||
|
||||
if format == "pdf" {
|
||||
let name = currentConversationName ?? "conversation"
|
||||
Task { @MainActor in
|
||||
do {
|
||||
let data = try await ConversationExportService.pdfData(name: name, messages: chatMessages)
|
||||
if let url = ConversationExportService.writeToDownloads(data, filename: filename) {
|
||||
showSystemMessage("Exported to \(url.path)")
|
||||
} else {
|
||||
showSystemMessage("Export failed: could not write file")
|
||||
}
|
||||
} catch {
|
||||
showSystemMessage("Export failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let content: String
|
||||
switch format {
|
||||
case "md", "markdown":
|
||||
content = chatMessages.map { msg in
|
||||
let header = msg.role == .user ? "**User**" : "**Assistant**"
|
||||
return "\(header)\n\n\(msg.content)"
|
||||
}.joined(separator: "\n\n---\n\n")
|
||||
content = ConversationExportService.markdown(messages: chatMessages)
|
||||
case "html":
|
||||
content = ConversationExportService.html(name: currentConversationName ?? "conversation", messages: chatMessages)
|
||||
case "json":
|
||||
let dicts = chatMessages.map { msg -> [String: String] in
|
||||
["role": msg.role.rawValue, "content": msg.content]
|
||||
@@ -1788,20 +1804,14 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
return
|
||||
}
|
||||
default:
|
||||
showSystemMessage("Unsupported format: \(format). Use md or json.")
|
||||
showSystemMessage("Unsupported format: \(format). Use md, html, pdf, or json.")
|
||||
return
|
||||
}
|
||||
|
||||
// Write to Downloads folder
|
||||
let downloads = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
|
||||
?? FileManager.default.temporaryDirectory
|
||||
let fileURL = downloads.appendingPathComponent(filename)
|
||||
|
||||
do {
|
||||
try content.write(to: fileURL, atomically: true, encoding: .utf8)
|
||||
showSystemMessage("Exported to \(fileURL.path)")
|
||||
} catch {
|
||||
showSystemMessage("Export failed: \(error.localizedDescription)")
|
||||
if let url = ConversationExportService.writeToDownloads(content, filename: filename) {
|
||||
showSystemMessage("Exported to \(url.path)")
|
||||
} else {
|
||||
showSystemMessage("Export failed: could not write file")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user