Let File > Export as HTML/PDF/Markdown choose name and location
Previously always silently wrote to Downloads with a fixed name — no way to pick where it goes or rename it. Added exportConversationWithSavePanel(format:defaultFilename:), which shows a native NSSavePanel (defaulting to Downloads + the same filename as before) before writing. Wired into all three File menu export buttons. The /export slash command and the conversation list's per-row Export submenu are unchanged (still write straight to Downloads) — the slash command already takes an explicit filename argument inline, and the row export is meant as a quick one-click action rather than a save-as workflow.
This commit is contained in:
@@ -24,6 +24,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import os
|
import os
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -1815,6 +1816,59 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Same export formats as `exportConversation(format:filename:)`, but lets the user pick
|
||||||
|
/// the name and location via a native save panel instead of always writing to Downloads.
|
||||||
|
#if os(macOS)
|
||||||
|
func exportConversationWithSavePanel(format: String, defaultFilename: String) {
|
||||||
|
let chatMessages = messages.filter { $0.role != .system }
|
||||||
|
guard !chatMessages.isEmpty else {
|
||||||
|
showSystemMessage("Nothing to export — no messages")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let panel = NSSavePanel()
|
||||||
|
panel.nameFieldStringValue = defaultFilename
|
||||||
|
panel.canCreateDirectories = true
|
||||||
|
panel.allowedContentTypes = {
|
||||||
|
switch format {
|
||||||
|
case "html": return [.html]
|
||||||
|
case "pdf": return [.pdf]
|
||||||
|
default: return [UTType(filenameExtension: "md") ?? .plainText]
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if let downloads = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first {
|
||||||
|
panel.directoryURL = downloads
|
||||||
|
}
|
||||||
|
|
||||||
|
guard panel.runModal() == .OK, let url = panel.url else { return }
|
||||||
|
|
||||||
|
if format == "pdf" {
|
||||||
|
let name = currentConversationName ?? "conversation"
|
||||||
|
Task { @MainActor in
|
||||||
|
do {
|
||||||
|
let data = try await ConversationExportService.pdfData(name: name, messages: chatMessages)
|
||||||
|
try data.write(to: url, options: .atomic)
|
||||||
|
showSystemMessage("Exported to \(url.path)")
|
||||||
|
} catch {
|
||||||
|
showSystemMessage("Export failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = format == "html"
|
||||||
|
? ConversationExportService.html(name: currentConversationName ?? "conversation", messages: chatMessages)
|
||||||
|
: ConversationExportService.markdown(messages: chatMessages)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try content.write(to: url, atomically: true, encoding: .utf8)
|
||||||
|
showSystemMessage("Exported to \(url.path)")
|
||||||
|
} catch {
|
||||||
|
showSystemMessage("Export failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// MARK: - Auto-Save & Background Summarization
|
// MARK: - Auto-Save & Background Summarization
|
||||||
|
|
||||||
/// Summarize the current conversation in the background (hidden from user)
|
/// Summarize the current conversation in the background (hidden from user)
|
||||||
|
|||||||
+3
-3
@@ -121,21 +121,21 @@ struct oAIApp: App {
|
|||||||
Button("Export as Markdown…") {
|
Button("Export as Markdown…") {
|
||||||
let name = chatViewModel.currentConversationName ?? "conversation"
|
let name = chatViewModel.currentConversationName ?? "conversation"
|
||||||
let safe = name.components(separatedBy: .whitespacesAndNewlines).joined(separator: "-")
|
let safe = name.components(separatedBy: .whitespacesAndNewlines).joined(separator: "-")
|
||||||
chatViewModel.exportConversation(format: "md", filename: "\(safe).md")
|
chatViewModel.exportConversationWithSavePanel(format: "md", defaultFilename: "\(safe).md")
|
||||||
}
|
}
|
||||||
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
|
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
|
||||||
|
|
||||||
Button("Export as HTML…") {
|
Button("Export as HTML…") {
|
||||||
let name = chatViewModel.currentConversationName ?? "conversation"
|
let name = chatViewModel.currentConversationName ?? "conversation"
|
||||||
let safe = name.components(separatedBy: .whitespacesAndNewlines).joined(separator: "-")
|
let safe = name.components(separatedBy: .whitespacesAndNewlines).joined(separator: "-")
|
||||||
chatViewModel.exportConversation(format: "html", filename: "\(safe).html")
|
chatViewModel.exportConversationWithSavePanel(format: "html", defaultFilename: "\(safe).html")
|
||||||
}
|
}
|
||||||
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
|
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
|
||||||
|
|
||||||
Button("Export as PDF…") {
|
Button("Export as PDF…") {
|
||||||
let name = chatViewModel.currentConversationName ?? "conversation"
|
let name = chatViewModel.currentConversationName ?? "conversation"
|
||||||
let safe = name.components(separatedBy: .whitespacesAndNewlines).joined(separator: "-")
|
let safe = name.components(separatedBy: .whitespacesAndNewlines).joined(separator: "-")
|
||||||
chatViewModel.exportConversation(format: "pdf", filename: "\(safe).pdf")
|
chatViewModel.exportConversationWithSavePanel(format: "pdf", defaultFilename: "\(safe).pdf")
|
||||||
}
|
}
|
||||||
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
|
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user