ChatGPT Message PDF Downloader
unknown
javascript
3 days ago
9.1 kB
2
No Index
// ==UserScript== // @name ChatGPT Message PDF Downloader (v3.3 - Final Layout Fix) // @namespace https://github.com/ // @version 3.3 // @description Simple, reliable PDF export for ChatGPT messages with fixes for duplication and page-breaks. // @author Your Name // @match https://chatgpt.com/* // @grant none // @require https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js // @license MIT // ==/UserScript== (function () { 'use strict'; const pdfIconSvg = ` <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon"> <path d="M11.5 2.5H5.5C4.94772 2.5 4.5 2.94772 4.5 3.5V16.5C4.5 17.0523 4.94772 17.5 5.5 17.5H14.5C15.0523 17.5 15.5 17.0523 15.5 16.5V6.5L11.5 2.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M11.5 2.5V6.5H15.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M7.5 12.5L10 15L12.5 12.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M10 15V9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> </svg> `; function addDownloadButton(turnElement) { const copyButton = turnElement.querySelector('button[data-testid*="copy"]'); if (!copyButton) return; const buttonContainer = copyButton.parentElement; if (!buttonContainer || buttonContainer.querySelector('.download-pdf-button')) return; const downloadBtn = document.createElement('button'); downloadBtn.className = 'text-token-text-secondary hover:bg-token-bg-secondary rounded-lg download-pdf-button'; downloadBtn.title = 'Download message as PDF'; downloadBtn.innerHTML = `<span class="touch:w-10 flex h-8 w-8 items-center justify-center">${pdfIconSvg}</span>`; downloadBtn.addEventListener('click', (e) => { e.stopPropagation(); generatePdfForMessage(turnElement); }); copyButton.insertAdjacentElement('afterend', downloadBtn); } function sanitizeFilename(name) { return name.replace(/[/\\?%*:|"<>]/g, '_'); } function extractAndCleanContent(turnElement) { const container = document.createElement('div'); container.style.cssText = ` max-width: 700px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; background: white; `; const messageContent = turnElement.querySelector('[data-message-author-role]') || turnElement; function processNode(node, targetParent) { if (node.nodeType === Node.TEXT_NODE) { targetParent.appendChild(document.createTextNode(node.textContent)); return; } if (node.nodeType !== Node.ELEMENT_NODE) return; if (node.tagName === 'BUTTON' || node.getAttribute('role') === 'button' || node.closest('[class*="react-scroll-to-bottom"]')) return; const tagName = node.tagName.toLowerCase(); let newElement; if (tagName === 'pre' || node.className.includes('language-')) { newElement = document.createElement('pre'); newElement.style.cssText = ` background: #f5f5f5; border: 1px solid #ddd; border-radius: 6px; padding: 12px; margin: 12px 0; font-family: 'Courier New', Consolas, monospace; font-size: 11px; line-height: 1.4; white-space: pre-wrap; word-break: break-all; overflow-wrap: anywhere; max-width: 100%; box-sizing: border-box; `; const codeText = node.textContent || ''; const wrappedText = codeText.replace(/(.{80})/g, '$1\n'); newElement.textContent = wrappedText; } else if (tagName === 'code' && node.parentElement.tagName !== 'PRE') { newElement = document.createElement('code'); newElement.style.cssText = ` background: #f0f0f0; padding: 2px 6px; border-radius: 4px; font-family: 'Courier New', Consolas, monospace; font-size: 11px; word-break: break-all; `; newElement.textContent = node.textContent || ''; } else if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tagName)) { newElement = document.createElement(tagName); newElement.style.cssText = `margin: 16px 0 8px 0; font-weight: bold; color: #222;`; } else if (tagName === 'p') { newElement = document.createElement('p'); newElement.style.margin = '12px 0'; } else if (['ul', 'ol'].includes(tagName)) { newElement = document.createElement(tagName); newElement.style.cssText = `margin: 12px 0; padding-left: 20px;`; } else if (tagName === 'li') { newElement = document.createElement('li'); newElement.style.margin = '4px 0'; } else if (tagName === 'strong' || tagName === 'b') { newElement = document.createElement('strong'); newElement.style.fontWeight = 'bold'; } else if (tagName === 'em' || tagName === 'i') { newElement = document.createElement('em'); newElement.style.fontStyle = 'italic'; } else { newElement = document.createElement('div'); } targetParent.appendChild(newElement); if (tagName === 'pre' || node.className.includes('language-')) { return; } for (const child of node.childNodes) { processNode(child, newElement); } } processNode(messageContent, container); return container; } async function generatePdfForMessage(turnElement) { try { if (typeof html2pdf === 'undefined') { alert('PDF library not loaded. Please refresh the page.'); return; } const cleanContent = extractAndCleanContent(turnElement); const htmlDoc = document.createElement('html'); const head = document.createElement('head'); const body = document.createElement('body'); const style = document.createElement('style'); style.textContent = ` @page { margin: 20mm; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; } h1, h2, h3, h4, h5, h6 { page-break-after: avoid !important; page-break-inside: avoid !important; } pre { background: #f8f8f8 !important; border: 1px solid #e0e0e0 !important; border-radius: 6px !important; padding: 12px !important; margin: 12px 0 !important; font-family: 'Courier New', Consolas, monospace !important; font-size: 11px !important; line-height: 1.4 !important; white-space: pre-wrap !important; word-break: break-all !important; overflow-wrap: anywhere !important; page-break-inside: avoid !important; } code { font-family: 'Courier New', Consolas, monospace !important; word-break: break-all !important; } `; head.appendChild(style); body.appendChild(cleanContent); htmlDoc.appendChild(head); htmlDoc.appendChild(body); const chatTitle = (document.title || 'ChatGPT').trim(); const firstLine = (turnElement.textContent || '').trim().split('\n')[0].slice(0, 60); const filename = sanitizeFilename(`${chatTitle} - ${firstLine}.pdf`); const opt = { margin: 0, filename, image: { type: 'jpeg', quality: 0.95 }, html2canvas: { scale: 1.5, useCORS: true, logging: false, backgroundColor: 'white' }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, pagebreak: { mode: ['css', 'legacy'], avoid: ['pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } }; await html2pdf().from(htmlDoc).set(opt).save(); } catch (error) { console.error('Error in generatePdfForMessage:', error); alert(`An error occurred: ${error.message || 'Unknown error'}`); } } function processAllMessages() { document.querySelectorAll('article[data-testid^="conversation-turn-"]').forEach(addDownloadButton); } let debounceTimer; const observer = new MutationObserver(() => { clearTimeout(debounceTimer); debounceTimer = setTimeout(processAllMessages, 200); }); function initialize() { if (typeof html2pdf === 'undefined') { console.error('html2pdf library not loaded. Check if the @require URL is accessible.'); setTimeout(initialize, 1000); return; } const mainContentArea = document.querySelector('main'); if (mainContentArea) { processAllMessages(); observer.observe(mainContentArea, { childList: true, subtree: true }); } else { setTimeout(initialize, 500); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } })();
Editor is loading...