Enhanced Claude Chat & Code Exporter 4.1

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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