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})();