Export your entire ChatGPT conversation archive as PDFs in a ZIP file with images
Size
33.1 KB
Version
1.1.2
Created
Oct 23, 2025
Updated
about 6 hours ago
1// ==UserScript==
2// @name ChatGPT Archive Exporter
3// @description Export your entire ChatGPT conversation archive as PDFs in a ZIP file with images
4// @version 1.1.2
5// @match https://chatgpt.com/*
6// @match https://*.chatgpt.com/*
7// @icon https://promptsloth.com/icon.ico?ee3a415f484dc50f
8// @grant GM.getValue
9// @grant GM.setValue
10// @grant GM.xmlhttpRequest
11// @require https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js
12// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
13// ==/UserScript==
14(function() {
15 'use strict';
16
17 // ============================================================================
18 // CHATGPT ARCHIVE EXPORTER - Complete Extension
19 // ============================================================================
20 // This extension exports your entire ChatGPT conversation archive as PDFs
21 // in a ZIP file with all embedded images preserved.
22 // Only activates when viewing Settings → Data Controls → Archived Chats
23 // ============================================================================
24
25 console.log('ChatGPT Archive Exporter loaded');
26
27 // Global state
28 let isExporting = false;
29 let shouldCancel = false;
30 let uiContainer = null;
31
32 // ============================================================================
33 // UTILITY FUNCTIONS
34 // ============================================================================
35
36 /**
37 * Debounce function to prevent excessive calls
38 */
39 function debounce(func, wait) {
40 let timeout;
41 return function executedFunction(...args) {
42 const later = () => {
43 clearTimeout(timeout);
44 func(...args);
45 };
46 clearTimeout(timeout);
47 timeout = setTimeout(later, wait);
48 };
49 }
50
51 /**
52 * Check if we're on the archived chats settings page
53 */
54 function isArchivedChatsPage() {
55 // Check URL hash
56 const isCorrectHash = window.location.hash.includes('ArchivedChats');
57
58 // Check for the modal
59 const modal = document.querySelector('[data-testid="modal-archived-conversations"]');
60
61 // Check for archived chats table
62 const hasTable = modal && modal.querySelector('table tbody tr') !== null;
63
64 console.log('Archived chats page check:', { isCorrectHash, hasModal: !!modal, hasTable });
65 return isCorrectHash && modal && hasTable;
66 }
67
68 /**
69 * Wait for element to appear in DOM
70 */
71 async function waitForElement(selector, timeout = 10000) {
72 const startTime = Date.now();
73 while (Date.now() - startTime < timeout) {
74 const element = document.querySelector(selector);
75 if (element) return element;
76 await new Promise(resolve => setTimeout(resolve, 100));
77 }
78 throw new Error(`Element ${selector} not found after ${timeout}ms`);
79 }
80
81 /**
82 * Convert image to data URL
83 */
84 async function imageToDataURL(imgElement) {
85 return new Promise((resolve) => {
86 try {
87 if (imgElement.src.startsWith('data:')) {
88 resolve(imgElement.src);
89 return;
90 }
91
92 const canvas = document.createElement('canvas');
93 const ctx = canvas.getContext('2d');
94
95 const img = new Image();
96 img.crossOrigin = 'anonymous';
97
98 img.onload = () => {
99 canvas.width = img.naturalWidth || img.width;
100 canvas.height = img.naturalHeight || img.height;
101 ctx.drawImage(img, 0, 0);
102 try {
103 const dataURL = canvas.toDataURL('image/png');
104 resolve(dataURL);
105 } catch (e) {
106 console.error('Canvas toDataURL failed:', e);
107 resolve(imgElement.src); // Fallback to original URL
108 }
109 };
110
111 img.onerror = () => {
112 console.error('Image load failed:', imgElement.src);
113 resolve(null);
114 };
115
116 img.src = imgElement.src;
117 } catch (error) {
118 console.error('imageToDataURL error:', error);
119 resolve(null);
120 }
121 });
122 }
123
124 /**
125 * Sanitize filename
126 */
127 function sanitizeFilename(name) {
128 return name
129 .replace(/[^a-z0-9_\-\.]/gi, '_')
130 .replace(/_{2,}/g, '_')
131 .substring(0, 200);
132 }
133
134 /**
135 * Format timestamp
136 */
137 function formatTimestamp() {
138 const now = new Date();
139 const year = now.getFullYear();
140 const month = String(now.getMonth() + 1).padStart(2, '0');
141 const day = String(now.getDate()).padStart(2, '0');
142 const hours = String(now.getHours()).padStart(2, '0');
143 const minutes = String(now.getMinutes()).padStart(2, '0');
144 const seconds = String(now.getSeconds()).padStart(2, '0');
145 return `${year}${month}${day}_${hours}${minutes}${seconds}`;
146 }
147
148 // ============================================================================
149 // DATA EXTRACTION
150 // ============================================================================
151
152 /**
153 * Extract all archived chat conversations from the settings table
154 */
155 async function extractChatsFromArchive() {
156 console.log('Starting chat extraction from archived chats settings page...');
157
158 // Wait for the modal to be ready
159 await new Promise(resolve => setTimeout(resolve, 1000));
160
161 const chats = [];
162
163 // Get the archived chats modal
164 const modal = document.querySelector('[data-testid="modal-archived-conversations"]');
165 if (!modal) {
166 throw new Error('Archived chats modal not found');
167 }
168
169 // Find all table rows with archived chats
170 const tableRows = modal.querySelectorAll('table tbody tr');
171 console.log(`Found ${tableRows.length} archived chat rows in table`);
172
173 if (tableRows.length === 0) {
174 throw new Error('No archived conversations found');
175 }
176
177 // Extract chat metadata from each row
178 for (let i = 0; i < tableRows.length; i++) {
179 const row = tableRows[i];
180
181 // Extract chat link and title
182 const linkElement = row.querySelector('a[href*="/c/"]');
183 if (!linkElement) continue;
184
185 const href = linkElement.getAttribute('href');
186 const chatIdMatch = href.match(/\/c\/([a-f0-9-]+)/);
187 const chatId = chatIdMatch ? chatIdMatch[1] : `chat_${i}`;
188
189 const title = linkElement.textContent.trim() || 'Untitled Chat';
190
191 // Extract date from second column
192 const dateCells = row.querySelectorAll('td');
193 const dateText = dateCells.length > 1 ? dateCells[1].textContent.trim() : new Date().toLocaleDateString();
194
195 // Build chat URL
196 const chatUrl = href.startsWith('http') ? href : `https://chatgpt.com${href}`;
197
198 chats.push({
199 id: chatId,
200 title: title,
201 date: dateText,
202 url: chatUrl,
203 element: row
204 });
205 }
206
207 console.log(`Extracted ${chats.length} archived chats from settings page`);
208 return chats;
209 }
210
211 /**
212 * Navigate to a chat and extract its full content
213 */
214 async function extractChatContent(chatUrl, chatId) {
215 console.log(`Extracting content for chat: ${chatId}`);
216
217 return new Promise((resolve) => {
218 // Open chat in new tab
219 const chatTab = window.open(chatUrl, '_blank');
220
221 if (!chatTab) {
222 console.error('Failed to open chat tab - popup blocked?');
223 resolve(null);
224 return;
225 }
226
227 // Wait for chat to load and extract content
228 setTimeout(async () => {
229 try {
230 // Try to access the chat tab's document
231 const chatDoc = chatTab.document;
232
233 // Wait for content to load
234 await new Promise(r => setTimeout(r, 3000));
235
236 // Extract messages
237 const messages = [];
238 const messageSelectors = [
239 '[data-testid*="conversation-turn"]',
240 '[data-message-author-role]',
241 'article[data-testid^="conversation-turn"]',
242 'div[class*="group"]'
243 ];
244
245 let messageElements = [];
246 for (const selector of messageSelectors) {
247 messageElements = chatDoc.querySelectorAll(selector);
248 if (messageElements.length > 0) {
249 console.log(`Found ${messageElements.length} messages using selector: ${selector}`);
250 break;
251 }
252 }
253
254 for (const msgEl of messageElements) {
255 // Determine if user or assistant message
256 const role = msgEl.getAttribute('data-message-author-role');
257 const isUser = role === 'user' ||
258 msgEl.querySelector('[data-message-author-role="user"]') !== null;
259
260 // Extract text content
261 const textElements = msgEl.querySelectorAll('p, pre, code, li');
262 let text = '';
263 if (textElements.length > 0) {
264 text = Array.from(textElements).map(el => el.textContent).join('\n');
265 } else {
266 text = msgEl.textContent.trim();
267 }
268
269 // Extract images
270 const images = [];
271 const imgElements = msgEl.querySelectorAll('img');
272 for (const img of imgElements) {
273 if (img.src && !img.src.includes('avatar') && !img.src.includes('icon')) {
274 const dataUrl = await imageToDataURL(img);
275 if (dataUrl) {
276 images.push({
277 src: img.src,
278 dataUrl: dataUrl,
279 alt: img.alt || 'Chat image'
280 });
281 }
282 }
283 }
284
285 if (text || images.length > 0) {
286 messages.push({
287 role: isUser ? 'user' : 'assistant',
288 text: text,
289 images: images
290 });
291 }
292 }
293
294 chatTab.close();
295 resolve({ messages });
296 } catch (error) {
297 console.error('Error extracting chat content:', error);
298 chatTab.close();
299 resolve(null);
300 }
301 }, 5000); // Increased timeout for better reliability
302 });
303 }
304
305 // ============================================================================
306 // PDF GENERATION
307 // ============================================================================
308
309 /**
310 * Generate PDF for a single chat
311 */
312 async function generateChatPDF(chat, content) {
313 console.log(`Generating PDF for chat: ${chat.title}`);
314
315 const { jsPDF } = window.jspdf;
316 const doc = new jsPDF({
317 orientation: 'portrait',
318 unit: 'mm',
319 format: 'a4'
320 });
321
322 const pageWidth = doc.internal.pageSize.getWidth();
323 const pageHeight = doc.internal.pageSize.getHeight();
324 const margin = 20;
325 const maxWidth = pageWidth - (margin * 2);
326
327 // ========================================================================
328 // PAGE 1: HEADER PAGE
329 // ========================================================================
330 doc.setFontSize(24);
331 doc.setFont(undefined, 'bold');
332 doc.text('ChatGPT Conversation', pageWidth / 2, 40, { align: 'center' });
333
334 doc.setFontSize(16);
335 doc.setFont(undefined, 'normal');
336 const titleLines = doc.splitTextToSize(chat.title, maxWidth);
337 doc.text(titleLines, pageWidth / 2, 60, { align: 'center' });
338
339 doc.setFontSize(12);
340 doc.text(`Date: ${chat.date}`, pageWidth / 2, 80, { align: 'center' });
341 doc.text(`Chat ID: ${chat.id}`, pageWidth / 2, 90, { align: 'center' });
342
343 if (!content || !content.messages || content.messages.length === 0) {
344 doc.setFontSize(10);
345 doc.text('No content available for this chat.', pageWidth / 2, 120, { align: 'center' });
346 return doc;
347 }
348
349 // ========================================================================
350 // PAGE 2: USER PROMPT
351 // ========================================================================
352 doc.addPage();
353
354 doc.setFontSize(18);
355 doc.setFont(undefined, 'bold');
356 doc.text('User Prompt', margin, 30);
357
358 doc.setFontSize(11);
359 doc.setFont(undefined, 'normal');
360
361 // Find first user message
362 const userMessages = content.messages.filter(m => m.role === 'user');
363 const firstUserMessage = userMessages[0];
364
365 if (firstUserMessage && firstUserMessage.text) {
366 const userTextLines = doc.splitTextToSize(firstUserMessage.text, maxWidth);
367 let yPosition = 45;
368
369 for (const line of userTextLines) {
370 if (yPosition > pageHeight - margin) {
371 doc.addPage();
372 yPosition = margin;
373 }
374 doc.text(line, margin, yPosition);
375 yPosition += 6;
376 }
377 } else {
378 doc.text('No user prompt found.', margin, 45);
379 }
380
381 // ========================================================================
382 // PAGE 3: AI RESPONSE
383 // ========================================================================
384 doc.addPage();
385
386 doc.setFontSize(18);
387 doc.setFont(undefined, 'bold');
388 doc.text('AI Response', margin, 30);
389
390 doc.setFontSize(11);
391 doc.setFont(undefined, 'normal');
392
393 // Find first assistant message
394 const assistantMessages = content.messages.filter(m => m.role === 'assistant');
395 const firstAssistantMessage = assistantMessages[0];
396
397 if (firstAssistantMessage && firstAssistantMessage.text) {
398 const aiTextLines = doc.splitTextToSize(firstAssistantMessage.text, maxWidth);
399 let yPosition = 45;
400
401 for (const line of aiTextLines) {
402 if (yPosition > pageHeight - margin) {
403 doc.addPage();
404 yPosition = margin;
405 }
406 doc.text(line, margin, yPosition);
407 yPosition += 6;
408 }
409 } else {
410 doc.text('No AI response found.', margin, 45);
411 }
412
413 // ========================================================================
414 // PAGES 4+: IMAGES
415 // ========================================================================
416 const allImages = [];
417 for (const message of content.messages) {
418 if (message.images && message.images.length > 0) {
419 allImages.push(...message.images);
420 }
421 }
422
423 for (let i = 0; i < allImages.length; i++) {
424 const image = allImages[i];
425
426 try {
427 doc.addPage();
428
429 doc.setFontSize(14);
430 doc.setFont(undefined, 'bold');
431 doc.text(`Image ${i + 1}`, margin, 20);
432
433 if (image.alt) {
434 doc.setFontSize(10);
435 doc.setFont(undefined, 'normal');
436 const altLines = doc.splitTextToSize(image.alt, maxWidth);
437 doc.text(altLines, margin, 28);
438 }
439
440 // Add image to PDF
441 if (image.dataUrl) {
442 const imgWidth = maxWidth;
443 const imgHeight = (maxWidth * 3) / 4; // Maintain aspect ratio
444 doc.addImage(image.dataUrl, 'PNG', margin, 40, imgWidth, imgHeight);
445 }
446 } catch (error) {
447 console.error(`Error adding image ${i + 1} to PDF:`, error);
448 }
449 }
450
451 return doc;
452 }
453
454 // ============================================================================
455 // ZIP PACKAGING
456 // ============================================================================
457
458 /**
459 * Create ZIP file with all PDFs and images
460 */
461 async function createZipArchive(pdfData, metadata) {
462 console.log('Creating ZIP archive...');
463
464 const zip = new JSZip();
465
466 // Add PDFs folder
467 const pdfsFolder = zip.folder('PDFs');
468 for (const pdf of pdfData) {
469 const filename = sanitizeFilename(pdf.title) + '.pdf';
470 pdfsFolder.file(filename, pdf.blob);
471 }
472
473 // Add images folder
474 const imagesFolder = zip.folder('images');
475 let imageIndex = 0;
476 for (const pdf of pdfData) {
477 if (pdf.images && pdf.images.length > 0) {
478 for (const img of pdf.images) {
479 if (img.dataUrl) {
480 const base64Data = img.dataUrl.split(',')[1];
481 const filename = `image_${imageIndex}_${sanitizeFilename(pdf.chatId)}.png`;
482 imagesFolder.file(filename, base64Data, { base64: true });
483 imageIndex++;
484 }
485 }
486 }
487 }
488
489 // Add metadata.json
490 zip.file('metadata.json', JSON.stringify(metadata, null, 2));
491
492 // Generate ZIP blob
493 const zipBlob = await zip.generateAsync({
494 type: 'blob',
495 compression: 'DEFLATE',
496 compressionOptions: { level: 6 }
497 });
498
499 return zipBlob;
500 }
501
502 /**
503 * Download ZIP file
504 */
505 function downloadZip(blob, filename) {
506 const url = URL.createObjectURL(blob);
507 const a = document.createElement('a');
508 a.href = url;
509 a.download = filename;
510 document.body.appendChild(a);
511 a.click();
512 document.body.removeChild(a);
513 URL.revokeObjectURL(url);
514 }
515
516 // ============================================================================
517 // MAIN EXPORT FUNCTION
518 // ============================================================================
519
520 /**
521 * Main export orchestration
522 */
523 async function exportArchive(progressCallback) {
524 if (isExporting) {
525 console.log('Export already in progress');
526 return;
527 }
528
529 isExporting = true;
530 shouldCancel = false;
531
532 try {
533 progressCallback({ status: 'Scanning archived chats...', progress: 0 });
534
535 // Step 1: Extract chat list from settings page
536 const chats = await extractChatsFromArchive();
537
538 if (chats.length === 0) {
539 throw new Error('No archived chats found.');
540 }
541
542 progressCallback({
543 status: `Found ${chats.length} archived chats. Starting export...`,
544 progress: 5
545 });
546
547 // Step 2: Process each chat
548 const pdfData = [];
549 const totalChats = chats.length;
550
551 for (let i = 0; i < totalChats; i++) {
552 if (shouldCancel) {
553 throw new Error('Export cancelled by user');
554 }
555
556 const chat = chats[i];
557 const progress = 5 + ((i / totalChats) * 70);
558
559 progressCallback({
560 status: `Processing chat ${i + 1}/${totalChats}: ${chat.title}`,
561 progress: Math.round(progress)
562 });
563
564 try {
565 // Extract chat content
566 const content = await extractChatContent(chat.url, chat.id);
567
568 if (!content) {
569 console.warn(`Failed to extract content for chat: ${chat.title}`);
570 continue;
571 }
572
573 // Generate PDF
574 const pdf = await generateChatPDF(chat, content);
575 const pdfBlob = pdf.output('blob');
576
577 pdfData.push({
578 title: chat.title,
579 chatId: chat.id,
580 blob: pdfBlob,
581 images: content.messages ?
582 content.messages.flatMap(m => m.images || []) : []
583 });
584
585 } catch (error) {
586 console.error(`Error processing chat ${chat.title}:`, error);
587 // Continue with next chat
588 }
589
590 // Add delay to avoid overwhelming the browser
591 await new Promise(resolve => setTimeout(resolve, 1000));
592 }
593
594 if (pdfData.length === 0) {
595 throw new Error('No chats were successfully exported');
596 }
597
598 // Step 3: Create ZIP
599 progressCallback({
600 status: 'Creating ZIP archive...',
601 progress: 80
602 });
603
604 const metadata = {
605 exportDate: new Date().toISOString(),
606 totalChats: pdfData.length,
607 exportedChats: pdfData.map(p => ({
608 title: p.title,
609 chatId: p.chatId
610 }))
611 };
612
613 const zipBlob = await createZipArchive(pdfData, metadata);
614
615 // Step 4: Download
616 progressCallback({
617 status: 'Downloading...',
618 progress: 95
619 });
620
621 const timestamp = formatTimestamp();
622 const filename = `ChatGPT_Archive_${timestamp}.zip`;
623 downloadZip(zipBlob, filename);
624
625 progressCallback({
626 status: `Export complete! Downloaded ${pdfData.length} chats.`,
627 progress: 100
628 });
629
630 // Store success state
631 await GM.setValue('lastExportDate', new Date().toISOString());
632 await GM.setValue('lastExportCount', pdfData.length);
633
634 } catch (error) {
635 console.error('Export failed:', error);
636 progressCallback({
637 status: `Export failed: ${error.message}`,
638 progress: 0,
639 error: true
640 });
641 } finally {
642 isExporting = false;
643 }
644 }
645
646 /**
647 * Cancel export
648 */
649 function cancelExport() {
650 shouldCancel = true;
651 console.log('Export cancellation requested');
652 }
653
654 // ============================================================================
655 // UI CREATION
656 // ============================================================================
657
658 /**
659 * Create and inject the export UI
660 */
661 function createExportUI() {
662 // Check if UI already exists
663 if (document.getElementById('chatgpt-export-ui')) {
664 return;
665 }
666
667 // Count archived chats
668 const modal = document.querySelector('[data-testid="modal-archived-conversations"]');
669 const chatCount = modal ? modal.querySelectorAll('table tbody tr').length : 0;
670
671 // Create UI container
672 const container = document.createElement('div');
673 container.id = 'chatgpt-export-ui';
674 container.style.cssText = `
675 position: fixed;
676 bottom: 20px;
677 right: 20px;
678 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
679 border-radius: 12px;
680 padding: 20px;
681 box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
682 z-index: 999999;
683 min-width: 320px;
684 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
685 color: white;
686 `;
687
688 // Prevent clicks from propagating to modal
689 container.addEventListener('click', (e) => {
690 e.stopPropagation();
691 });
692 container.addEventListener('mousedown', (e) => {
693 e.stopPropagation();
694 });
695
696 // Title
697 const title = document.createElement('h3');
698 title.textContent = 'Export Archived Chats';
699 title.style.cssText = `
700 margin: 0 0 10px 0;
701 font-size: 18px;
702 font-weight: 600;
703 `;
704 container.appendChild(title);
705
706 // Info text
707 const infoText = document.createElement('div');
708 infoText.style.cssText = `
709 font-size: 12px;
710 opacity: 0.9;
711 margin-bottom: 15px;
712 line-height: 1.4;
713 `;
714 infoText.textContent = `Export all ${chatCount} archived conversations as PDFs in a ZIP file.`;
715 container.appendChild(infoText);
716
717 // Export button
718 const exportBtn = document.createElement('button');
719 exportBtn.id = 'export-archive-btn';
720 exportBtn.textContent = 'Start Export';
721 exportBtn.style.cssText = `
722 width: 100%;
723 padding: 12px;
724 background: white;
725 color: #667eea;
726 border: none;
727 border-radius: 8px;
728 font-size: 16px;
729 font-weight: 600;
730 cursor: pointer;
731 transition: all 0.3s ease;
732 margin-bottom: 10px;
733 `;
734 exportBtn.onmouseover = () => {
735 exportBtn.style.transform = 'translateY(-2px)';
736 exportBtn.style.boxShadow = '0 5px 15px rgba(0, 0, 0, 0.2)';
737 };
738 exportBtn.onmouseout = () => {
739 exportBtn.style.transform = 'translateY(0)';
740 exportBtn.style.boxShadow = 'none';
741 };
742 container.appendChild(exportBtn);
743
744 // Progress container
745 const progressContainer = document.createElement('div');
746 progressContainer.id = 'export-progress';
747 progressContainer.style.cssText = `
748 display: none;
749 margin-top: 15px;
750 `;
751
752 // Progress bar
753 const progressBar = document.createElement('div');
754 progressBar.style.cssText = `
755 width: 100%;
756 height: 8px;
757 background: rgba(255, 255, 255, 0.3);
758 border-radius: 4px;
759 overflow: hidden;
760 margin-bottom: 10px;
761 `;
762
763 const progressFill = document.createElement('div');
764 progressFill.id = 'progress-fill';
765 progressFill.style.cssText = `
766 width: 0%;
767 height: 100%;
768 background: white;
769 transition: width 0.3s ease;
770 `;
771 progressBar.appendChild(progressFill);
772 progressContainer.appendChild(progressBar);
773
774 // Progress text
775 const progressText = document.createElement('div');
776 progressText.id = 'progress-text';
777 progressText.style.cssText = `
778 font-size: 13px;
779 opacity: 0.9;
780 margin-bottom: 10px;
781 `;
782 progressContainer.appendChild(progressText);
783
784 // Cancel button
785 const cancelBtn = document.createElement('button');
786 cancelBtn.id = 'cancel-export-btn';
787 cancelBtn.textContent = 'Cancel';
788 cancelBtn.style.cssText = `
789 width: 100%;
790 padding: 8px;
791 background: rgba(255, 255, 255, 0.2);
792 color: white;
793 border: 1px solid rgba(255, 255, 255, 0.3);
794 border-radius: 6px;
795 font-size: 14px;
796 cursor: pointer;
797 transition: all 0.3s ease;
798 `;
799 cancelBtn.onmouseover = () => {
800 cancelBtn.style.background = 'rgba(255, 255, 255, 0.3)';
801 };
802 cancelBtn.onmouseout = () => {
803 cancelBtn.style.background = 'rgba(255, 255, 255, 0.2)';
804 };
805 progressContainer.appendChild(cancelBtn);
806
807 container.appendChild(progressContainer);
808
809 // Privacy note
810 const privacyNote = document.createElement('div');
811 privacyNote.style.cssText = `
812 font-size: 10px;
813 opacity: 0.7;
814 margin-top: 10px;
815 line-height: 1.3;
816 `;
817 privacyNote.textContent = '🔒 All processing happens locally in your browser. No data is sent anywhere.';
818 container.appendChild(privacyNote);
819
820 // Add to page
821 document.body.appendChild(container);
822 uiContainer = container;
823
824 // Event listeners
825 exportBtn.addEventListener('click', async (e) => {
826 e.preventDefault();
827 e.stopPropagation();
828 e.stopImmediatePropagation();
829
830 exportBtn.style.display = 'none';
831 progressContainer.style.display = 'block';
832
833 await exportArchive((update) => {
834 const progressFill = document.getElementById('progress-fill');
835 const progressText = document.getElementById('progress-text');
836
837 if (progressFill) {
838 progressFill.style.width = `${update.progress}%`;
839 }
840
841 if (progressText) {
842 progressText.textContent = update.status;
843 if (update.error) {
844 progressText.style.color = '#ffcccc';
845 }
846 }
847
848 if (update.progress === 100 || update.error) {
849 setTimeout(() => {
850 exportBtn.style.display = 'block';
851 progressContainer.style.display = 'none';
852 if (progressFill) progressFill.style.width = '0%';
853 if (progressText) {
854 progressText.textContent = '';
855 progressText.style.color = 'white';
856 }
857 }, 3000);
858 }
859 });
860 });
861
862 cancelBtn.addEventListener('click', (e) => {
863 e.preventDefault();
864 e.stopPropagation();
865 e.stopImmediatePropagation();
866 cancelExport();
867 });
868
869 console.log('Export UI created and injected');
870 }
871
872 /**
873 * Remove export UI
874 */
875 function removeExportUI() {
876 if (uiContainer && uiContainer.parentNode) {
877 uiContainer.parentNode.removeChild(uiContainer);
878 uiContainer = null;
879 console.log('Export UI removed');
880 }
881 }
882
883 // ============================================================================
884 // ARCHIVED CHATS PAGE MONITORING
885 // ============================================================================
886
887 /**
888 * Monitor archived chats page and show/hide UI accordingly
889 */
890 function monitorArchivedChatsPage() {
891 const checkPage = debounce(() => {
892 const pageOpen = isArchivedChatsPage();
893 const uiExists = document.getElementById('chatgpt-export-ui') !== null;
894
895 if (pageOpen && !uiExists) {
896 console.log('Archived chats page opened - showing export UI');
897 createExportUI();
898 } else if (!pageOpen && uiExists) {
899 console.log('Archived chats page closed - hiding export UI');
900 removeExportUI();
901 }
902 }, 300);
903
904 // Watch for URL hash changes
905 window.addEventListener('hashchange', checkPage);
906
907 // Use MutationObserver to watch for modal changes
908 const observer = new MutationObserver(checkPage);
909 observer.observe(document.body, {
910 childList: true,
911 subtree: true
912 });
913
914 // Also check periodically as backup
915 setInterval(checkPage, 2000);
916
917 // Initial check
918 setTimeout(checkPage, 1000);
919
920 console.log('Started monitoring archived chats page');
921 }
922
923 // ============================================================================
924 // INITIALIZATION
925 // ============================================================================
926
927 /**
928 * Initialize the extension
929 */
930 function init() {
931 console.log('Initializing ChatGPT Archive Exporter...');
932
933 // Wait for page to be ready
934 if (document.readyState === 'loading') {
935 document.addEventListener('DOMContentLoaded', init);
936 return;
937 }
938
939 // Check if we're on ChatGPT
940 if (!window.location.hostname.includes('chatgpt.com')) {
941 console.log('Not on ChatGPT, extension inactive');
942 return;
943 }
944
945 // Wait for the page to fully load
946 setTimeout(() => {
947 monitorArchivedChatsPage();
948 }, 2000);
949
950 console.log('ChatGPT Archive Exporter initialized successfully');
951 }
952
953 // Start the extension
954 init();
955
956})();