diff --git a/oAI/Services/ConversationExportService.swift b/oAI/Services/ConversationExportService.swift
index ea886c1..04c7b6d 100644
--- a/oAI/Services/ConversationExportService.swift
+++ b/oAI/Services/ConversationExportService.swift
@@ -38,36 +38,6 @@ enum ConversationExportService {
// MARK: - HTML
- nonisolated static func html(name: String, messages: [Message]) -> String {
- let body = messages.map { msg -> String in
- let roleLabel = msg.role == .user ? "User" : "Assistant"
- let roleClass = msg.role == .user ? "user" : "assistant"
- let rendered = renderMarkdownBody(htmlEscape(msg.content))
- return """
-
-
\(roleLabel)
- \(rendered)
-
- """
- }.joined(separator: "\n")
-
- return """
-
-
-
-
-
- \(htmlEscape(name))
-
-
-
- \(htmlEscape(name))
- \(body)
-
-
- """
- }
-
nonisolated private static let css = """
:root { color-scheme: light dark; }
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
@@ -114,6 +84,77 @@ enum ConversationExportService {
}
"""
+ // PDF is baked at export time — it can't respond to prefers-color-scheme live like an
+ // HTML file opened in a browser can, and empirically the print pipeline ignores that media
+ // query entirely regardless (confirmed: identical CSS printed light even with the webview
+ // forced into dark appearance). So PDF gets its own always-dark stylesheet, applied
+ // unconditionally rather than behind a media query. print-color-adjust: exact is required
+ // too — browsers/WebKit's print pipeline strips background colors by default to save ink
+ // unless told otherwise; without it every background here silently prints white.
+ nonisolated private static let pdfCss = """
+ * { -webkit-print-color-adjust: exact; print-color-adjust: exact; color-adjust: exact; }
+ :root { color-scheme: dark; }
+ html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
+ ::-webkit-scrollbar { display: none; }
+ body { font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; \
+ font-size: 14px; color: #e8e8e8; background: #1c1c1e; max-width: 820px; margin: 40px auto; \
+ padding: 0 24px; line-height: 1.5; }
+ h1 { font-size: 22px; border-bottom: 1px solid #3a3a3c; padding-bottom: 12px; }
+ .message { margin: 20px 0; padding: 12px 16px; border-radius: 8px; border-left: 4px solid transparent; \
+ break-inside: avoid; page-break-inside: avoid; }
+ .message.user { background: #24303d; border-left-color: #5aa2f0; }
+ .message.assistant { background: #262626; border-left-color: #9a9a9a; }
+ .role { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; \
+ color: #a8a8a8; margin-bottom: 8px; }
+ .message p { margin: 8px 0; }
+ .message h1 { font-size: 19px; margin: 12px 0 6px; }
+ .message h2 { font-size: 17px; margin: 12px 0 6px; }
+ .message h3 { font-size: 15.5px; margin: 12px 0 6px; }
+ .message h4, .message h5, .message h6 { font-size: 14px; margin: 12px 0 6px; }
+ .message ul, .message ol { margin: 8px 0; padding-left: 24px; }
+ .message blockquote { margin: 8px 0; padding: 4px 12px; border-left: 3px solid #555; color: #bbb; }
+ .message code { font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 0.9em; \
+ background: #2e2e2e; color: #e0e0e0; padding: 1px 5px; border-radius: 4px; }
+ .message pre { background: #111; color: #e8e8e8; padding: 12px 14px; border-radius: 6px; \
+ overflow-x: auto; }
+ .message pre code { background: none; padding: 0; color: inherit; }
+ .message hr { border: none; border-top: 1px solid #3a3a3c; margin: 16px 0; }
+ .message a { color: #6fb1f0; }
+ .message table { border-collapse: collapse; margin: 10px 0; width: 100%; }
+ .message th, .message td { border: 1px solid #3a3a3c; padding: 6px 10px; text-align: left; }
+ .message th { background: #2a2a2c; font-weight: 600; }
+ """
+
+ nonisolated static func html(name: String, messages: [Message], forceDarkCSS: Bool = false) -> String {
+ let body = messages.map { msg -> String in
+ let roleLabel = msg.role == .user ? "User" : "Assistant"
+ let roleClass = msg.role == .user ? "user" : "assistant"
+ let rendered = renderMarkdownBody(htmlEscape(msg.content))
+ return """
+
+
\(roleLabel)
+ \(rendered)
+
+ """
+ }.joined(separator: "\n")
+
+ return """
+
+
+
+
+
+ \(htmlEscape(name))
+
+
+
+ \(htmlEscape(name))
+ \(body)
+
+
+ """
+ }
+
// MARK: - PDF
enum PDFError: LocalizedError {
@@ -128,15 +169,12 @@ enum ConversationExportService {
/// conversation). That's a fundamentally different, non-standard document shape from any
/// normal PDF, which is what actually made ordinarily-sized text read as "huge" — there was
/// no page of a familiar size to judge it against. Printing through NSPrintOperation with a
- /// real US Letter paper size gives genuine multi-page pagination, matching what any other
- /// app's "export/print to PDF" produces.
+ /// real A4 paper size gives genuine multi-page pagination, matching what any other app's
+ /// "export/print to PDF" produces.
static func pdfData(name: String, messages: [Message]) async throws -> Data {
- let htmlString = html(name: name, messages: messages)
+ let htmlString = html(name: name, messages: messages, forceDarkCSS: true)
let webView = WKWebView(frame: NSRect(x: 0, y: 0, width: 850, height: 1100))
webView.pageZoom = 1.0
- // PDF content is baked at export time, so it can't respond to prefers-color-scheme
- // live like the HTML export does — match whatever appearance the app is in right now.
- webView.appearance = NSApp.effectiveAppearance
let delegate = PDFLoadDelegate()
webView.navigationDelegate = delegate
try await delegate.load(htmlString, in: webView)