ChatGPT Project Bulk PDF Exporter

Export all conversations from a ChatGPT project folder as separate PDF files

Size

12.8 KB

Version

1.0.1

Created

Oct 16, 2025

Updated

7 days ago

1// ==UserScript==
2// @name		ChatGPT Project Bulk PDF Exporter
3// @description		Export all conversations from a ChatGPT project folder as separate PDF files
4// @version		1.0.1
5// @match		https://*.chatgpt.com/*
6// @icon		https://cdn.oaistatic.com/assets/favicon-l4nq08hd.svg
7// @require		https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js
8// @require		https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
9// @require		https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
10// ==/UserScript==
11(function() {
12    'use strict';
13
14    console.log('ChatGPT Project Bulk PDF Exporter loaded');
15
16    // Utility function to sanitize filename
17    function sanitizeFilename(filename) {
18        return filename.replace(/[^a-z0-9]/gi, '_').replace(/_+/g, '_').substring(0, 100);
19    }
20
21    // Utility function to wait for element
22    function waitForElement(selector, timeout = 10000) {
23        return new Promise((resolve, reject) => {
24            const startTime = Date.now();
25            const checkInterval = setInterval(() => {
26                const element = document.querySelector(selector);
27                if (element) {
28                    clearInterval(checkInterval);
29                    resolve(element);
30                } else if (Date.now() - startTime > timeout) {
31                    clearInterval(checkInterval);
32                    reject(new Error(`Element ${selector} not found within ${timeout}ms`));
33                }
34            }, 100);
35        });
36    }
37
38    // Function to extract conversation content from a page
39    async function extractConversationContent(conversationUrl) {
40        console.log('Extracting content from:', conversationUrl);
41        
42        return new Promise((resolve, reject) => {
43            const iframe = document.createElement('iframe');
44            iframe.style.display = 'none';
45            iframe.src = conversationUrl;
46            
47            iframe.onload = async () => {
48                try {
49                    await new Promise(resolve => setTimeout(resolve, 3000)); // Wait for content to load
50                    
51                    const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
52                    
53                    // Find the main conversation container
54                    const conversationContainer = iframeDoc.querySelector('main') || iframeDoc.querySelector('[role="main"]') || iframeDoc.body;
55                    
56                    if (!conversationContainer) {
57                        throw new Error('Could not find conversation container');
58                    }
59                    
60                    // Extract all message elements
61                    const messages = Array.from(conversationContainer.querySelectorAll('[data-message-author-role], .group\\/conversation-turn, article'));
62                    
63                    const conversationData = {
64                        title: iframeDoc.title || 'Untitled Conversation',
65                        messages: messages.map(msg => ({
66                            text: msg.textContent?.trim() || '',
67                            html: msg.innerHTML || ''
68                        }))
69                    };
70                    
71                    document.body.removeChild(iframe);
72                    resolve(conversationData);
73                } catch (error) {
74                    document.body.removeChild(iframe);
75                    reject(error);
76                }
77            };
78            
79            iframe.onerror = () => {
80                document.body.removeChild(iframe);
81                reject(new Error('Failed to load conversation'));
82            };
83            
84            document.body.appendChild(iframe);
85        });
86    }
87
88    // Function to generate PDF from conversation data
89    async function generatePDF(conversationData) {
90        console.log('Generating PDF for:', conversationData.title);
91        
92        const { jsPDF } = window.jspdf;
93        const pdf = new jsPDF({
94            orientation: 'portrait',
95            unit: 'mm',
96            format: 'a4'
97        });
98        
99        const pageWidth = pdf.internal.pageSize.getWidth();
100        const pageHeight = pdf.internal.pageSize.getHeight();
101        const margin = 15;
102        const maxWidth = pageWidth - (margin * 2);
103        let yPosition = margin;
104        
105        // Add title
106        pdf.setFontSize(16);
107        pdf.setFont(undefined, 'bold');
108        const titleLines = pdf.splitTextToSize(conversationData.title, maxWidth);
109        pdf.text(titleLines, margin, yPosition);
110        yPosition += titleLines.length * 7 + 10;
111        
112        // Add messages
113        pdf.setFontSize(11);
114        pdf.setFont(undefined, 'normal');
115        
116        for (const message of conversationData.messages) {
117            if (!message.text) continue;
118            
119            // Check if we need a new page
120            if (yPosition > pageHeight - margin - 20) {
121                pdf.addPage();
122                yPosition = margin;
123            }
124            
125            const messageLines = pdf.splitTextToSize(message.text, maxWidth);
126            
127            // Check if message fits on current page
128            const messageHeight = messageLines.length * 5;
129            if (yPosition + messageHeight > pageHeight - margin) {
130                pdf.addPage();
131                yPosition = margin;
132            }
133            
134            pdf.text(messageLines, margin, yPosition);
135            yPosition += messageHeight + 8;
136        }
137        
138        return pdf;
139    }
140
141    // Function to export all conversations in a project
142    async function exportProjectConversations() {
143        try {
144            console.log('Starting project export...');
145            
146            // Update button state
147            const exportBtn = document.getElementById('export-project-btn');
148            if (exportBtn) {
149                exportBtn.disabled = true;
150                exportBtn.textContent = 'Exporting...';
151            }
152            
153            // Get all conversation links in the project
154            const conversationLinks = Array.from(document.querySelectorAll('section ol li a[href*="/c/"]'));
155            
156            if (conversationLinks.length === 0) {
157                alert('No conversations found in this project.');
158                return;
159            }
160            
161            console.log(`Found ${conversationLinks.length} conversations`);
162            
163            const conversations = conversationLinks.map(link => ({
164                title: link.querySelector('.text-sm.font-medium')?.textContent?.trim() || 'Untitled',
165                url: link.href,
166                id: link.href.split('/c/')[1]
167            }));
168            
169            // Create a zip file to store all PDFs
170            const zip = new JSZip();
171            
172            // Process each conversation
173            for (let i = 0; i < conversations.length; i++) {
174                const conv = conversations[i];
175                
176                if (exportBtn) {
177                    exportBtn.textContent = `Exporting ${i + 1}/${conversations.length}...`;
178                }
179                
180                console.log(`Processing conversation ${i + 1}/${conversations.length}: ${conv.title}`);
181                
182                try {
183                    // Extract conversation content
184                    const conversationData = await extractConversationContent(conv.url);
185                    
186                    // Generate PDF
187                    const pdf = await generatePDF(conversationData);
188                    
189                    // Add PDF to zip
190                    const filename = sanitizeFilename(conv.title) + '.pdf';
191                    const pdfBlob = pdf.output('blob');
192                    zip.file(filename, pdfBlob);
193                    
194                    console.log(`Successfully exported: ${conv.title}`);
195                } catch (error) {
196                    console.error(`Failed to export ${conv.title}:`, error);
197                    // Continue with next conversation
198                }
199                
200                // Add delay to avoid rate limiting
201                await new Promise(resolve => setTimeout(resolve, 1000));
202            }
203            
204            // Generate and download the zip file
205            console.log('Generating zip file...');
206            if (exportBtn) {
207                exportBtn.textContent = 'Creating ZIP...';
208            }
209            
210            const zipBlob = await zip.generateAsync({ type: 'blob' });
211            const downloadLink = document.createElement('a');
212            downloadLink.href = URL.createObjectURL(zipBlob);
213            
214            // Get project name from page title
215            const projectName = document.title.replace('ChatGPT - ', '').trim();
216            downloadLink.download = `${sanitizeFilename(projectName)}_conversations.zip`;
217            
218            document.body.appendChild(downloadLink);
219            downloadLink.click();
220            document.body.removeChild(downloadLink);
221            
222            console.log('Export completed successfully!');
223            alert(`Successfully exported ${conversations.length} conversations!`);
224            
225            if (exportBtn) {
226                exportBtn.disabled = false;
227                exportBtn.textContent = 'Export Project to PDF';
228            }
229            
230        } catch (error) {
231            console.error('Export failed:', error);
232            alert('Export failed: ' + error.message);
233            
234            const exportBtn = document.getElementById('export-project-btn');
235            if (exportBtn) {
236                exportBtn.disabled = false;
237                exportBtn.textContent = 'Export Project to PDF';
238            }
239        }
240    }
241
242    // Function to add export button to project page
243    function addExportButton() {
244        // Check if we're on a project page
245        if (!window.location.pathname.includes('/project')) {
246            console.log('Not on a project page, skipping button injection');
247            return;
248        }
249        
250        // Check if button already exists
251        if (document.getElementById('export-project-btn')) {
252            console.log('Export button already exists');
253            return;
254        }
255        
256        console.log('Adding export button to project page');
257        
258        // Find the project header or a suitable location
259        const projectHeader = document.querySelector('header') || document.querySelector('main');
260        
261        if (!projectHeader) {
262            console.error('Could not find suitable location for export button');
263            return;
264        }
265        
266        // Create button container
267        const buttonContainer = document.createElement('div');
268        buttonContainer.id = 'export-project-container';
269        buttonContainer.style.cssText = `
270            position: fixed;
271            top: 20px;
272            right: 20px;
273            z-index: 9999;
274            background: white;
275            padding: 10px;
276            border-radius: 8px;
277            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
278        `;
279        
280        // Create export button
281        const exportButton = document.createElement('button');
282        exportButton.id = 'export-project-btn';
283        exportButton.textContent = 'Export Project to PDF';
284        exportButton.style.cssText = `
285            background: #10a37f;
286            color: white;
287            border: none;
288            padding: 10px 20px;
289            border-radius: 6px;
290            font-size: 14px;
291            font-weight: 600;
292            cursor: pointer;
293            transition: background 0.2s;
294        `;
295        
296        exportButton.onmouseover = () => {
297            exportButton.style.background = '#0d8c6d';
298        };
299        
300        exportButton.onmouseout = () => {
301            exportButton.style.background = '#10a37f';
302        };
303        
304        exportButton.onclick = exportProjectConversations;
305        
306        buttonContainer.appendChild(exportButton);
307        document.body.appendChild(buttonContainer);
308        
309        console.log('Export button added successfully');
310    }
311
312    // Initialize the extension
313    function init() {
314        console.log('Initializing ChatGPT Project Bulk PDF Exporter');
315        
316        // Wait for page to load
317        if (document.readyState === 'loading') {
318            document.addEventListener('DOMContentLoaded', () => {
319                setTimeout(addExportButton, 2000);
320            });
321        } else {
322            setTimeout(addExportButton, 2000);
323        }
324        
325        // Watch for navigation changes (SPA)
326        let lastUrl = location.href;
327        new MutationObserver(() => {
328            const currentUrl = location.href;
329            if (currentUrl !== lastUrl) {
330                lastUrl = currentUrl;
331                console.log('URL changed to:', currentUrl);
332                setTimeout(addExportButton, 2000);
333            }
334        }).observe(document.body, { childList: true, subtree: true });
335    }
336
337    // Start the extension
338    init();
339})();
ChatGPT Project Bulk PDF Exporter | Robomonkey