Enhanced Claude Chat & Code Exporter 4.1

Export Claude chat conversations with code artifacts into individual files with timestamp prefixes

// ==UserScript==
// @name         Enhanced Claude Chat & Code Exporter 4.1
// @namespace    http://tampermonkey.net/
// @version      4.1
// @description  Export Claude chat conversations with code artifacts into individual files with timestamp prefixes
// @author       Claude
// @match        https://claude.ai/chat/*
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// @grant        GM_download
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT 
// ==/UserScript==

(function() {
    'use strict';

    // Add export buttons to the UI
    function addExportButtons() {
        // Check if export buttons already exist
        if (document.querySelector('.claude-export-button')) {
            return;
        }

        // Find a good location for the buttons (next to other input controls)
        const inputControls = document.querySelector('.flex-row.items-center.gap-2');

        if (inputControls) {
            // Create button container
            const buttonContainer = document.createElement('div');
            buttonContainer.className = 'claude-export-button';
            buttonContainer.style.display = 'flex';
            buttonContainer.style.alignItems = 'center';
            buttonContainer.style.gap = '6px';

            // Create the export all button
            const exportAllButton = document.createElement('button');
            exportAllButton.type = 'button';
            exportAllButton.className = 'inline-flex items-center justify-center relative rounded-lg px-3 h-8 text-white';
            exportAllButton.style.backgroundColor = '#4a6ee0';
            exportAllButton.style.marginLeft = '8px';
            exportAllButton.style.display = 'flex';
            exportAllButton.style.alignItems = 'center';
            exportAllButton.style.fontFamily = 'system-ui, -apple-system, sans-serif';
            exportAllButton.style.fontSize = '14px';
            exportAllButton.style.fontWeight = '500';
            exportAllButton.setAttribute('aria-label', 'Export All');

            // Button content with icon and text
            exportAllButton.innerHTML = `
                <div class="flex items-center gap-1">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256" style="margin-right: 4px;">
                        <path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112a8,8,0,0,1,16,0v96H200V112a8,8,0,0,1,16,0ZM80,80a8,8,0,0,1,8-8h32V36a4,4,0,0,1,4-4h8a4,4,0,0,1,4,4V72h32a8,8,0,0,1,5.66,13.66l-40,40a8,8,0,0,1-11.32,0l-40-40A8,8,0,0,1,80,80Z"></path>
                    </svg>
                    <span>Export All</span>
                </div>
            `;

            // Add click event listener
            exportAllButton.addEventListener('click', exportConversation);

            // Create the markdown-only export button
            const exportMdButton = document.createElement('button');
            exportMdButton.type = 'button';
            exportMdButton.className = 'inline-flex items-center justify-center relative rounded-lg px-3 h-8 text-white';
            exportMdButton.style.backgroundColor = '#9e6ee0'; // Different color to distinguish from Export All
            exportMdButton.style.display = 'flex';
            exportMdButton.style.alignItems = 'center';
            exportMdButton.style.fontFamily = 'system-ui, -apple-system, sans-serif';
            exportMdButton.style.fontSize = '14px';
            exportMdButton.style.fontWeight = '500';
            exportMdButton.setAttribute('aria-label', 'Export Markdown Only');

            // Button content with icon and text
            exportMdButton.innerHTML = `
                <div class="flex items-center gap-1">
                    <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256" style="margin-right: 4px;">
                        <path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112a8,8,0,0,1,16,0v96H200V112a8,8,0,0,1,16,0ZM80,80a8,8,0,0,1,8-8h32V36a4,4,0,0,1,4-4h8a4,4,0,0,1,4,4V72h32a8,8,0,0,1,5.66,13.66l-40,40a8,8,0,0,1-11.32,0l-40-40A8,8,0,0,1,80,80Z"></path>
                    </svg>
                    <span>Md Only</span>
                </div>
            `;

            // Add click event listener
            exportMdButton.addEventListener('click', exportMarkdownOnly);

            // Add the buttons to the DOM
            buttonContainer.appendChild(exportAllButton);
            buttonContainer.appendChild(exportMdButton);
            inputControls.appendChild(buttonContainer);
        }
    }

    // Helper function to download a file
    function downloadFile(blob, filename) {
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();

        // Cleanup
        setTimeout(() => {
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }, 100);
    }

    // Generate a timestamp in the format yyyyMMddHHmmss
    function generateTimestamp() {
        const now = new Date();
        const year = now.getFullYear();
        const month = String(now.getMonth() + 1).padStart(2, '0');
        const day = String(now.getDate()).padStart(2, '0');
        const hours = String(now.getHours()).padStart(2, '0');
        const minutes = String(now.getMinutes()).padStart(2, '0');
        const seconds = String(now.getSeconds()).padStart(2, '0');

        return `${year}${month}${day}${hours}${minutes}${seconds}`;
    }

    // Function to export only the markdown content
    async function exportMarkdownOnly() {
        try {
            logDebug("Starting markdown-only export process...");

            // Show loading indicator
            showLoadingIndicator("Extracting conversation...");

            // Generate timestamp prefix for this export
            const timestampPrefix = generateTimestamp();
            logDebug(`Generated timestamp prefix: ${timestampPrefix}`);

            // Get the chat title
            const chatTitle = getChatTitle();
            const safeChatTitle = sanitizeFileName(chatTitle || 'Claude Conversation');
            logDebug(`Chat title: ${chatTitle} (sanitized: ${safeChatTitle})`);

            // Extract the conversation as markdown
            logDebug("Extracting conversation as markdown");
            const markdown = extractConversationMarkdown();

            // Download the markdown file with timestamp prefix
            const markdownFilename = `${timestampPrefix}_${safeChatTitle}.md`;
            const markdownBlob = new Blob([markdown], { type: 'text/markdown' });
            downloadFile(markdownBlob, markdownFilename);
            logDebug(`Downloaded markdown file: ${markdownFilename}`);

            // Show success message
            hideLoadingIndicator();
            showNotification(`Exported Claude conversation as markdown successfully!`, "success");

        } catch (error) {
            logDebug(`Error in exportMarkdownOnly: ${error.message}`);
            console.error('Error exporting markdown:', error);
            hideLoadingIndicator();
            showNotification('Error exporting markdown. Check console for details.', "error");
        }
    }

    // Main function to export the conversation with artifacts
    async function exportConversation() {
        try {
            logDebug("Starting export process...");

            // Initialize a variable to store clipboard content for artifact extraction
            let savedClipboardContent = "";

            // Try to save current clipboard content so we can restore it later
            try {
                savedClipboardContent = await navigator.clipboard.readText();
                logDebug("Saved original clipboard content");
            } catch (error) {
                logDebug("Could not read original clipboard content: " + error.message);
            }

            // Show loading indicator
            showLoadingIndicator("Processing chat and artifacts...");

            // Generate timestamp prefix for this export session
            const timestampPrefix = generateTimestamp();
            logDebug(`Generated timestamp prefix: ${timestampPrefix}`);

            // Get the chat title
            const chatTitle = getChatTitle();
            const safeChatTitle = sanitizeFileName(chatTitle || 'Claude Conversation');
            logDebug(`Chat title: ${chatTitle} (sanitized: ${safeChatTitle})`);

            // Extract the conversation as markdown
            logDebug("Extracting conversation as markdown");
            const markdown = extractConversationMarkdown();

            // Download the markdown file with timestamp prefix
            const markdownFilename = `${timestampPrefix}_00_${safeChatTitle}.md`;
            const markdownBlob = new Blob([markdown], { type: 'text/markdown' });
            downloadFile(markdownBlob, markdownFilename);
            logDebug(`Downloaded markdown file: ${markdownFilename}`);

            // Small delay before processing artifacts
            await new Promise(resolve => setTimeout(resolve, 300));

            // Find all artifact containers in the DOM
            const artifactButtons = document.querySelectorAll('button[aria-label="Preview contents"]');
            logDebug(`Found ${artifactButtons.length} artifact buttons`);

            // Process all artifacts sequentially
            showLoadingIndicator(`Found ${artifactButtons.length} artifacts, processing...`);

            for (let i = 0; i < artifactButtons.length; i++) {
                const artifactButton = artifactButtons[i];

                try {
                    // Update loading indicator with progress
                    showLoadingIndicator(`Processing artifact ${i+1} of ${artifactButtons.length}...`);

                    // First, extract metadata without opening the artifact
                    const initialArtifact = extractArtifactMetadataFromPreview(artifactButton, i);

                    if (!initialArtifact) {
                        logDebug(`Failed to extract metadata for artifact ${i+1}`);
                        continue;
                    }

                    // Click the artifact button to open the code panel
                    logDebug(`Clicking artifact button ${i+1} to open code panel`);
                    artifactButton.click();

                    // Wait for the code panel to load
                    await new Promise(resolve => setTimeout(resolve, 1000));

                    // Now extract the full content using keyboard shortcut method
                    const fullArtifact = await extractArtifactUsingKeyboardCopy(initialArtifact);

                    if (fullArtifact && fullArtifact.content) {
                        const artifactNumber = String(i + 1).padStart(2, '0');
                        const fileName = `${timestampPrefix}_${artifactNumber}_${sanitizeFileName(fullArtifact.title)}${getFileExtension(fullArtifact.language)}`;

                        // Download the artifact
                        const blob = new Blob([fullArtifact.content], { type: 'text/plain' });
                        downloadFile(blob, fileName);

                        logDebug(`Downloaded artifact ${i+1}: ${fileName} (${fullArtifact.content.length} chars)`);

                        // Close the code panel by clicking outside or on close button
                        const closeButton = document.querySelector('button svg[width="18"][height="18"] path[d*="205.66,194.34"]');
                        if (closeButton && closeButton.parentElement && closeButton.parentElement.parentElement) {
                            closeButton.parentElement.parentElement.click();
                        } else {
                            // If can't find the close button, try clicking elsewhere
                            const header = document.querySelector('header');
                            if (header) header.click();
                        }

                        // Small delay between artifacts to prevent browser throttling
                        await new Promise(resolve => setTimeout(resolve, 800));
                    } else {
                        logDebug(`Failed to extract content for artifact ${i+1}`);
                    }
                } catch (error) {
                    logDebug(`Error processing artifact ${i+1}: ${error.message}`);
                    console.error(`Error processing artifact ${i+1}:`, error);

                    // Try to close any open panels before continuing
                    const closeButton = document.querySelector('button svg[width="18"][height="18"] path[d*="205.66,194.34"]');
                    if (closeButton && closeButton.parentElement && closeButton.parentElement.parentElement) {
                        closeButton.parentElement.parentElement.click();
                    }
                }
            }

            // Try to restore original clipboard content
            if (savedClipboardContent) {
                try {
                    await navigator.clipboard.writeText(savedClipboardContent);
                    logDebug("Restored original clipboard content");
                } catch (error) {
                    logDebug("Could not restore clipboard: " + error.message);
                }
            }

            // Show success message
            hideLoadingIndicator();
            showNotification(`Exported Claude conversation and ${artifactButtons.length} artifacts successfully!`, "success");

        } catch (error) {
            logDebug(`Error in exportConversation: ${error.message}`);
            console.error('Error exporting conversation:', error);
            hideLoadingIndicator();
            showNotification('Error exporting conversation. Check console for details.', "error");
        }
    }

    // Extract only metadata from an artifact preview without opening it
    function extractArtifactMetadataFromPreview(button, index) {
        try {
            // Extract metadata from the preview
            const titleElement = button.querySelector('.leading-tight.text-sm');
            const typeElement = button.querySelector('.text-sm.text-text-300');

            let title = `artifact_${index + 1}`;
            let type = 'Code';

            if (titleElement) {
                title = titleElement.textContent.trim();
            }

            if (typeElement) {
                type = typeElement.textContent.trim();
            }

            // Return metadata without content
            return {
                title: title,
                type: type,
                language: determineLanguage(type, title, ""),
                content: null // We'll get the content later
            };
        } catch (err) {
            logDebug(`Error in extractArtifactMetadataFromPreview: ${err.message}`);
            console.error('Error extracting artifact metadata from preview:', err);
            return null;
        }
    }

    // Extract artifact content using keyboard shortcut Ctrl+A, Ctrl+C
    async function extractArtifactUsingKeyboardCopy(artifactMetadata) {
        try {
            // Look for the code block in the panel
            const codeBlock = document.querySelector('.code-block__code');
            if (!codeBlock) {
                logDebug("No code block found in panel");
                return null;
            }

            // Try to determine language from the code element
            let language = artifactMetadata.language;
            const codeElement = codeBlock.querySelector('code');
            if (codeElement && codeElement.className) {
                const match = codeElement.className.match(/language-(\w+)/);
                if (match && match[1]) {
                    language = match[1];
                    logDebug(`Detected language: ${language}`);
                }
            }

            // Focus on the code element or code block to prepare for keyboard commands
            if (codeElement) {
                codeElement.focus();
                logDebug("Focused on code element");
            } else {
                codeBlock.focus();
                logDebug("Focused on code block");
            }

            // Wait a bit for the focus to take effect
            await new Promise(resolve => setTimeout(resolve, 200));

            // Approach 1: Direct copy via document.execCommand
            // (works in many browsers even with clipboard restrictions)
            try {
                // Select all text in the code block
                const selection = window.getSelection();
                const range = document.createRange();

                // Clear any existing selection
                selection.removeAllRanges();

                // Create range for the entire code element or block
                range.selectNodeContents(codeElement || codeBlock);

                // Add the range to selection
                selection.addRange(range);

                // Execute copy command
                const copySuccessful = document.execCommand('copy');

                if (copySuccessful) {
                    logDebug("Successfully copied via document.execCommand");
                } else {
                    logDebug("document.execCommand('copy') returned false");
                }

                // Clear selection
                selection.removeAllRanges();

                // Wait for clipboard to be updated
                await new Promise(resolve => setTimeout(resolve, 300));
            } catch (err) {
                logDebug(`Error using execCommand copy: ${err.message}`);
            }

            // Approach 2: Try to find and use the existing copy button
            try {
                // Look for the copy button within the toolbar
                const copyButton = document.querySelector('.flex.border.font-medium .py-1.px-2, button[class*="py-1 px-2 border-r border-border-300"]');

                if (copyButton) {
                    logDebug("Found copy button, clicking it");
                    copyButton.click();

                    // Wait for clipboard to be updated
                    await new Promise(resolve => setTimeout(resolve, 300));
                } else {
                    logDebug("Copy button not found");
                }
            } catch (err) {
                logDebug(`Error clicking copy button: ${err.message}`);
            }

            // Now try to get clipboard content
            let clipboardData = null;
            try {
                clipboardData = await navigator.clipboard.readText();
                logDebug(`Successfully read from clipboard: ${clipboardData.length} characters`);
            } catch (error) {
                logDebug(`Error reading from clipboard: ${error.message}`);

                // Fall back to text extraction method as last resort
                const extractedText = extractAllTextFromElement(codeElement || codeBlock);
                logDebug(`Using fallback extraction method: ${extractedText.length} chars`);
                return {
                    title: artifactMetadata.title,
                    type: artifactMetadata.type,
                    language: language,
                    content: extractedText
                };
            }

            // If we got data, use it
            if (clipboardData && clipboardData.length > 0) {
                return {
                    title: artifactMetadata.title,
                    type: artifactMetadata.type,
                    language: language,
                    content: clipboardData
                };
            } else {
                logDebug("No clipboard data obtained");

                // Fall back to text extraction method as last resort
                const extractedText = extractAllTextFromElement(codeElement || codeBlock);
                logDebug(`Using fallback extraction method: ${extractedText ? extractedText.length : 0} chars`);
                return {
                    title: artifactMetadata.title,
                    type: artifactMetadata.type,
                    language: language,
                    content: extractedText || "// Error extracting content"
                };
            }

        } catch (err) {
            logDebug(`Error in extractArtifactUsingKeyboardCopy: ${err.message}`);
            console.error('Error extracting artifact using keyboard copy:', err);

            // Try to find code element again for fallback
            const codeBlock = document.querySelector('.code-block__code');
            const codeElement = codeBlock ? codeBlock.querySelector('code') : null;

            // Determine language again in case it wasn't set earlier
            let language = artifactMetadata.language;
            if (codeElement && codeElement.className) {
                const match = codeElement.className.match(/language-(\w+)/);
                if (match && match[1]) {
                    language = match[1];
                }
            }

            // Fall back to text extraction method
            const extractedText = extractAllTextFromElement(codeElement || codeBlock);
            logDebug(`Using fallback extraction method after error: ${extractedText ? extractedText.length : 0} chars`);

            return {
                title: artifactMetadata.title,
                type: artifactMetadata.type,
                language: language,
                content: extractedText || "// Error extracting content"
            };
        }
    }

    // Extract all text from an element including all child nodes, maintaining line breaks
    function extractAllTextFromElement(element) {
        if (!element) return "";

        let text = '';
        const childNodes = element.childNodes;

        for (let i = 0; i < childNodes.length; i++) {
            const node = childNodes[i];

            if (node.nodeType === Node.TEXT_NODE) {
                text += node.textContent;
            } else if (node.nodeType === Node.ELEMENT_NODE) {
                // Process element nodes
                if (node.tagName === 'BR' || node.tagName === 'DIV' || node.tagName === 'P') {
                    text += '\n'; // Add newline for line break elements
                }

                // Recursively process child elements
                text += extractAllTextFromElement(node);

                // Add newline after certain block elements
                if (node.tagName === 'DIV' || node.tagName === 'P' ||
                    node.tagName === 'LI' || node.tagName === 'TR') {
                    text += '\n';
                }
            }
        }

        return text;
    }

    // Extract the conversation as markdown
    function extractConversationMarkdown() {
        let markdown = '';

        // Get chat title
        const chatTitle = getChatTitle();
        if (chatTitle) {
            markdown += `# ${chatTitle}\n\n`;
        }

        // Add export timestamp
        const now = new Date();
        markdown += `*Exported on: ${now.toLocaleString()}*\n\n`;

        // Get all message containers
        const messageContainers = document.querySelectorAll('[data-test-render-count="1"]');

        messageContainers.forEach(container => {
            // Check if this is a user message
            const userMessage = container.querySelector('[data-testid="user-message"]');
            if (userMessage) {
                markdown += `## User\n\n${userMessage.textContent.trim()}\n\n`;
                return;
            }

            // Check if this is a Claude message
            const claudeMessage = container.querySelector('.font-claude-message');
            if (claudeMessage) {
                markdown += `## Claude\n\n`;

                // Get all paragraphs and headings in the message
                const elements = claudeMessage.querySelectorAll('p, h1, h2, h3, h4, ul, ol, li, blockquote, pre, code');

                elements.forEach(element => {
                    if (element.tagName === 'H1') {
                        markdown += `### ${element.textContent.trim()}\n\n`;
                    } else if (element.tagName === 'H2') {
                        markdown += `#### ${element.textContent.trim()}\n\n`;
                    } else if (element.tagName === 'H3') {
                        markdown += `##### ${element.textContent.trim()}\n\n`;
                    } else if (element.tagName === 'P') {
                        markdown += `${element.textContent.trim()}\n\n`;
                    } else if (element.tagName === 'UL') {
                        // We'll handle list items individually
                    } else if (element.tagName === 'OL') {
                        // We'll handle list items individually
                    } else if (element.tagName === 'LI') {
                        const depth = parseInt(element.getAttribute('depth') || '0');
                        const indent = '  '.repeat(depth);
                        markdown += `${indent}- ${element.textContent.trim()}\n`;
                    } else if (element.tagName === 'BLOCKQUOTE') {
                        markdown += `> ${element.textContent.trim()}\n\n`;
                    } else if (element.tagName === 'PRE' || element.tagName === 'CODE') {
                        // Skip code blocks as they'll be exported separately
                        // However, inline code should still be included
                        if (element.classList.contains('bg-text-200/5')) {
                            markdown += `\`${element.textContent.trim()}\``;
                        }
                    }
                });

                // Add a reference to each code artifact
                const artifactButtons = claudeMessage.querySelectorAll('button[aria-label="Preview contents"]');
                artifactButtons.forEach((button, index) => {
                    const titleElement = button.querySelector('.leading-tight.text-sm');
                    const typeElement = button.querySelector('.text-sm.text-text-300');

                    if (titleElement && typeElement) {
                        const title = titleElement.textContent.trim();
                        const type = typeElement.textContent.trim();
                        const artifactNumber = String(index + 1).padStart(2, '0');

                        markdown += `\n**Code Artifact:** \`${artifactNumber}_${title}\` (${type})\n`;
                        markdown += `*See separate file with corresponding timestamp prefix*\n\n`;
                    }
                });

                markdown += '\n\n';
            }
        });

        return markdown;
    }

    // Determine the language of a code artifact based on context clues
    function determineLanguage(type, title, content) {
        // If it's not code, return as document
        if (type.toLowerCase() !== 'code') {
            return 'markdown';
        }

        // Check title for language hints
        const titleLower = title.toLowerCase();
        if (titleLower.includes('java')) return 'java';
        if (titleLower.includes('python') || titleLower.includes('.py')) return 'python';
        if (titleLower.includes('javascript') || titleLower.includes('js')) return 'javascript';
        if (titleLower.includes('html')) return 'html';
        if (titleLower.includes('css')) return 'css';
        if (titleLower.includes('bash') || titleLower.includes('shell') || titleLower.includes('.sh')) return 'bash';
        if (titleLower.includes('powershell') || titleLower.includes('.ps1')) return 'powershell';
        if (titleLower.includes('sql')) return 'sql';
        if (titleLower.includes('c#')) return 'csharp';
        if (titleLower.includes('c++')) return 'cpp';
        if (titleLower.includes('go')) return 'go';
        if (titleLower.includes('rust')) return 'rust';

        // Check content for language clues if content is provided
        if (content) {
            if (content.includes('public class') || content.includes('import java.')) return 'java';
            if (content.includes('def ') && content.includes(':')) return 'python';
            if (content.includes('function') && content.includes('{')) return 'javascript';
            if (content.includes('<html') || content.includes('<!DOCTYPE html')) return 'html';
            if (content.includes('#!/bin/bash')) return 'bash';
            if (content.includes('#!/bin/sh')) return 'bash';
            if (content.includes('#!powershell')) return 'powershell';
        }

        // Default to plaintext if we can't determine
        return 'plaintext';
    }

    // Get the appropriate file extension for a language
    function getFileExtension(language) {
        const extensions = {
            'java': '.java',
            'python': '.py',
            'javascript': '.js',
            'html': '.html',
            'css': '.css',
            'bash': '.sh',
            'powershell': '.ps1',
            'sql': '.sql',
            'csharp': '.cs',
            'cpp': '.cpp',
            'go': '.go',
            'rust': '.rs',
            'markdown': '.md',
            'plaintext': '.txt'
        };

        return extensions[language.toLowerCase()] || '.txt';
    }

    // Get the chat title
    function getChatTitle() {
        const titleElement = document.querySelector('.truncate.tracking-tight.font-normal.font-styrene');
        return titleElement ? titleElement.textContent.trim() : null;
    }

    // Sanitize a string to be used as a filename
    function sanitizeFileName(name) {
        return name
            .replace(/[\\/:*?"<>|]/g, '_') // Replace invalid filename chars
            .replace(/\s+/g, '_')          // Replace spaces with underscores
            .replace(/__+/g, '_')          // Replace multiple underscores with a single one
            .replace(/^_+|_+$/g, '')       // Remove leading/trailing underscores
            .slice(0, 100);                // Limit length to 100 chars
    }

    // Log debug information to console with prefix
    function logDebug(message) {
        console.log(`[Claude Exporter] ${message}`);
    }

    // Show loading indicator with message
    function showLoadingIndicator(message) {
        // Remove existing indicator if any
        hideLoadingIndicator();

        const indicator = document.createElement('div');
        indicator.id = 'claude-export-loading';
        indicator.style.position = 'fixed';
        indicator.style.top = '50%';
        indicator.style.left = '50%';
        indicator.style.transform = 'translate(-50%, -50%)';
        indicator.style.padding = '20px';
        indicator.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
        indicator.style.color = 'white';
        indicator.style.borderRadius = '8px';
        indicator.style.zIndex = '10000';
        indicator.style.fontSize = '16px';
        indicator.style.fontFamily = 'system-ui, -apple-system, sans-serif';

        // Add a spinner and message for better visual feedback
        indicator.innerHTML = `
            <div style="display: flex; align-items: center; gap: 10px;">
                <div class="spinner" style="border: 3px solid rgba(255,255,255,.3); border-radius: 50%; border-top: 3px solid white; width: 20px; height: 20px; animation: spin 1s linear infinite;"></div>
                <div>${message || 'Processing...'}</div>
            </div>
        `;

        // Add animation style
        const style = document.createElement('style');
        style.id = 'claude-export-style';
        style.textContent = `
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }
        `;

        if (!document.getElementById('claude-export-style')) {
            document.head.appendChild(style);
        }

        document.body.appendChild(indicator);
    }

    // Hide loading indicator
    function hideLoadingIndicator() {
        const indicator = document.getElementById('claude-export-loading');
        if (indicator) {
            document.body.removeChild(indicator);
        }
    }

    // Show a notification
    function showNotification(message, type = "info") {
        // Remove any existing notification
        const existingNotification = document.getElementById('claude-export-notification');
        if (existingNotification) {
            document.body.removeChild(existingNotification);
        }

        const notification = document.createElement('div');
        notification.id = 'claude-export-notification';
        notification.style.position = 'fixed';
        notification.style.bottom = '20px';
        notification.style.left = '50%';
        notification.style.transform = 'translateX(-50%)';
        notification.style.padding = '10px 20px';
        notification.style.borderRadius = '4px';
        notification.style.zIndex = '10000';
        notification.style.fontSize = '14px';
        notification.style.fontFamily = 'system-ui, -apple-system, sans-serif';
        notification.style.textAlign = 'center';
        notification.style.maxWidth = '80%';
        notification.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.2)';

        if (type === "error") {
            notification.style.backgroundColor = '#f44336';
            notification.style.color = 'white';
        } else if (type === "success") {
            notification.style.backgroundColor = '#4CAF50';
            notification.style.color = 'white';
        } else {
            notification.style.backgroundColor = '#2196F3';
            notification.style.color = 'white';
        }

        notification.textContent = message;

        document.body.appendChild(notification);

        // Remove after 5 seconds
        setTimeout(() => {
            if (document.getElementById('claude-export-notification')) {
                document.body.removeChild(notification);
            }
        }, 5000);
    }

    // Initialize the script
    function init() {
        logDebug("Initializing Enhanced Claude Exporter 4.1");

        // Add export buttons when the page loads
        addExportButtons();

        // Create a MutationObserver to watch for DOM changes
        const observer = new MutationObserver(() => {
            // Check if we need to add the export buttons after DOM changes
            addExportButtons();
        });

        // Start observing the document body for changes
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        // Also register menu commands
        GM_registerMenuCommand('Export Claude Conversation with Artifacts', exportConversation);
        GM_registerMenuCommand('Export Claude Conversation as Markdown Only', exportMarkdownOnly);

        logDebug("Initialization complete");
    }

    // Run the initialization
    init();
})();