ChatGPT Archive Exporter

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})();
ChatGPT Archive Exporter | Robomonkey