Enhanced Claude.Ai Export v2.1

Export Claude conversations with multiple formats, better structure, and robust element detection

// ==UserScript==
// @name     Enhanced Claude.Ai Export v2.1
// @description Export Claude conversations with multiple formats, better structure, and robust element detection
// @version  2.1
// @author   Original: TheAlanK & SAPIENT | Enhanced by:iikoshteruu 
// @grant    none
// @match    *://claude.ai/*
// @namespace https://github.com/iikoshteruu/Enhanced-Claude.Ai-Export-v2.0
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    console.log('Enhanced Claude Export v2.1 starting...');

    // Configuration
    const CONFIG = {
        buttonText: 'Export Full',
        formats: ['txt', 'md', 'json'],
        defaultFormat: 'md',
        debug: true,
        autoScroll: true,
        scrollDelay: 1000, // Wait time between scroll attempts
        maxScrollAttempts: 50 // Maximum scroll attempts to prevent infinite loops
    };

    let isExporting = false;

    function debugLog(message, data = null) {
        if (CONFIG.debug) {
            console.log('[Claude Export v2.1]', message, data || '');
        }
    }

    // Auto-scroll to load all conversation content
    async function loadFullConversation() {
        debugLog('Starting full conversation loading...');

        return new Promise((resolve) => {
            let scrollAttempts = 0;
            let lastScrollHeight = 0;
            let unchangedCount = 0;

            const scrollInterval = setInterval(() => {
                // Scroll to top to load older messages
                window.scrollTo(0, 0);

                // Get current scroll height
                const currentScrollHeight = document.body.scrollHeight;

                debugLog(`Scroll attempt ${scrollAttempts + 1}, Height: ${currentScrollHeight}`);

                // Check if height changed (new content loaded)
                if (currentScrollHeight === lastScrollHeight) {
                    unchangedCount++;
                } else {
                    unchangedCount = 0;
                    lastScrollHeight = currentScrollHeight;
                }

                scrollAttempts++;

                // Stop if we've tried enough times or height hasn't changed for a while
                if (scrollAttempts >= CONFIG.maxScrollAttempts || unchangedCount >= 3) {
                    clearInterval(scrollInterval);
                    debugLog(`Scroll complete. Total attempts: ${scrollAttempts}`);

                    // Scroll through entire conversation to make sure everything is loaded
                    setTimeout(() => {
                        window.scrollTo(0, 0);
                        setTimeout(() => {
                            window.scrollTo(0, document.body.scrollHeight);
                            setTimeout(() => {
                                resolve();
                            }, 1000);
                        }, 500);
                    }, 500);
                }
            }, CONFIG.scrollDelay);
        });
    }

    // Enhanced conversation detection with multiple strategies
    function getConversationData() {
        debugLog('Starting enhanced conversation data extraction...');
        const messages = [];
        let allTextContent = [];

        // Strategy 1: Try to find main conversation container
        const conversationContainer = document.querySelector('main') ||
                                    document.querySelector('[data-testid="conversation"]') ||
                                    document.querySelector('.conversation') ||
                                    document.body;

        debugLog('Using container:', conversationContainer.tagName);

        // Strategy 2: Multiple selector approaches
        const selectors = [
            // Look for message containers
            'div[class*="col-start-2"]',
            'div[class*="message"]',
            'div[data-testid*="message"]',
            '[role="presentation"] > div',
            // Look for content areas
            'div[class*="prose"]',
            'div[class*="markdown"]',
            'div[class*="content"]',
            // Look for any div with substantial text content
            'div'
        ];

        let messageElements = [];

        for (const selector of selectors) {
            try {
                const elements = Array.from(conversationContainer.querySelectorAll(selector));
                const validElements = elements.filter(el => {
                    const text = el.textContent?.trim() || '';
                    // Must have substantial text content
                    return text.length > 20 &&
                           text.length < 50000 &&
                           // Exclude navigation, buttons, etc.
                           !el.closest('nav, header, footer, button, input, select') &&
                           // Exclude elements that are likely UI components
                           !text.includes('Export') &&
                           !text.includes('New chat') &&
                           !text.includes('Settings');
                });

                debugLog(`Selector "${selector}" found ${validElements.length} valid elements`);

                if (validElements.length > 0) {
                    messageElements = validElements;
                    break;
                }
            } catch (error) {
                debugLog(`Selector "${selector}" failed:`, error.message);
            }
        }

        // Strategy 3: If no good elements found, try a different approach
        if (messageElements.length === 0) {
            debugLog('No elements found, trying fallback approach...');

            // Get all divs and filter by text content patterns
            const allDivs = Array.from(document.querySelectorAll('div'));
            messageElements = allDivs.filter(div => {
                const text = div.textContent?.trim() || '';
                return text.length > 50 &&
                       text.length < 20000 &&
                       !div.querySelector('input, button, nav, header, footer') &&
                       // Look for conversation-like patterns
                       (text.includes('Claude') ||
                        text.includes('Human') ||
                        text.includes('I ') ||
                        text.includes('You ') ||
                        text.match(/^(Hi|Hello|Can you|Could you|Please|What|How|Why)/i));
            });
        }

        debugLog(`Processing ${messageElements.length} message elements...`);

        // Process found elements
        const processedTexts = new Set(); // Avoid duplicates

        messageElements.forEach((element, index) => {
            try {
                const clone = element.cloneNode(true);

                // Remove unwanted elements more aggressively
                const unwanted = clone.querySelectorAll(
                    'svg, button, input, select, nav, header, footer, script, style, ' +
                    '[aria-hidden="true"], [class*="icon"], [class*="button"], ' +
                    '[class*="menu"], [class*="toolbar"], [class*="nav"]'
                );
                unwanted.forEach(el => el.remove());

                const text = clone.textContent?.trim() || '';

                if (text && text.length > 10 && !processedTexts.has(text)) {
                    processedTexts.add(text);

                    // Better speaker detection
                    const speaker = detectSpeaker(element, text, index, messageElements.length);

                    messages.push({
                        speaker: speaker,
                        content: text,
                        timestamp: new Date().toISOString(),
                        index: index,
                        length: text.length
                    });

                    debugLog(`Message ${index + 1}: ${speaker} (${text.length} chars)`);
                }
            } catch (error) {
                debugLog(`Error processing element ${index}:`, error.message);
            }
        });

        // Sort messages by their position in the DOM (top to bottom)
        messages.sort((a, b) => a.index - b.index);

        debugLog(`Extracted ${messages.length} unique messages`);
        return messages;
    }

    // Improved speaker detection
    function detectSpeaker(element, text, index, totalMessages) {
        // Check element classes and attributes
        const classes = element.className?.toLowerCase() || '';
        const parentClasses = element.parentElement?.className?.toLowerCase() || '';
        const testId = element.getAttribute('data-testid') || '';

        // Explicit indicators
        if (classes.includes('user') || parentClasses.includes('user') || testId.includes('user')) {
            return 'Human';
        }
        if (classes.includes('assistant') || parentClasses.includes('assistant') || testId.includes('assistant')) {
            return 'Claude';
        }

        // Content-based detection
        const humanPatterns = [
            /^(hi|hello|hey|can you|could you|please|help me|i need|i want|how do|what is|why)/i,
            /\?$/, // Questions often end with ?
            text.length < 200, // Humans often write shorter messages
            /^(ok|okay|thanks|thank you|great|perfect)/i
        ];

        const claudePatterns = [
            /^(i'll|i can|i'd be happy|here's|let me|i understand|certainly|of course|absolutely)/i,
            /```/, // Code blocks
            text.length > 500, // Claude often writes longer responses
            /^(looking at|based on|here are|to help you)/i,
            text.includes('🎯') || text.includes('📝') || text.includes('🔧') // Emojis often used by Claude
        ];

        let humanScore = 0;
        let claudeScore = 0;

        humanPatterns.forEach(pattern => {
            if (typeof pattern === 'boolean') {
                if (pattern) humanScore++;
            } else {
                if (pattern.test(text)) humanScore++;
            }
        });

        claudePatterns.forEach(pattern => {
            if (typeof pattern === 'boolean') {
                if (pattern) claudeScore++;
            } else {
                if (pattern.test(text)) claudeScore++;
            }
        });

        // If scores are tied, use alternating pattern (assuming conversation starts with human)
        if (humanScore === claudeScore) {
            return index % 2 === 0 ? 'Human' : 'Claude';
        }

        return humanScore > claudeScore ? 'Human' : 'Claude';
    }

    // Format as plain text with better structure
    function formatAsText(messages) {
        if (messages.length === 0) return 'No conversation found.';

        let output = `Claude.ai COMPLETE Conversation Export\n`;
        output += `Exported: ${new Date().toLocaleString()}\n`;
        output += `Total Messages: ${messages.length}\n`;
        output += `URL: ${window.location.href}\n`;
        output += '='.repeat(80) + '\n\n';

        messages.forEach((msg, index) => {
            output += `${msg.speaker}:\n`;
            output += `${msg.content}\n\n`;

            if (index < messages.length - 1) {
                output += '-'.repeat(50) + '\n\n';
            }
        });

        return output;
    }

    // Format as Markdown with better structure
    function formatAsMarkdown(messages) {
        if (messages.length === 0) return '# No conversation found';

        let md = `# Claude.ai Complete Conversation Export\n\n`;
        md += `**Exported:** ${new Date().toLocaleString()}  \n`;
        md += `**Total Messages:** ${messages.length}  \n`;
        md += `**URL:** ${window.location.href}  \n`;
        md += `**Export Method:** Enhanced Auto-Scroll v2.1\n\n`;
        md += `---\n\n`;

        messages.forEach(msg => {
            md += `## ${msg.speaker}\n\n`;
            md += `${msg.content}\n\n`;
        });

        return md;
    }

    // Format as JSON with full metadata
    function formatAsJSON(messages) {
        const exportData = {
            exportDate: new Date().toISOString(),
            exportTimestamp: Date.now(),
            exportVersion: '2.1',
            messageCount: messages.length,
            url: window.location.href,
            userAgent: navigator.userAgent,
            conversation: messages,
            statistics: {
                humanMessages: messages.filter(m => m.speaker === 'Human').length,
                claudeMessages: messages.filter(m => m.speaker === 'Claude').length,
                totalCharacters: messages.reduce((sum, m) => sum + m.content.length, 0),
                averageMessageLength: Math.round(messages.reduce((sum, m) => sum + m.content.length, 0) / messages.length)
            }
        };
        return JSON.stringify(exportData, null, 2);
    }

    // Download file with better error handling
    function downloadFile(content, filename, type = 'text/plain') {
        try {
            debugLog(`Downloading: ${filename} (${content.length} chars)`);

            const blob = new Blob([content], { type: type + ';charset=utf-8' });
            const url = URL.createObjectURL(blob);
            const link = document.createElement('a');

            link.download = filename;
            link.href = url;
            link.style.display = 'none';

            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);

            setTimeout(() => URL.revokeObjectURL(url), 1000);

            debugLog('Download completed successfully');
            return true;
        } catch (error) {
            console.error('Download failed:', error);
            alert(`Download failed: ${error.message}`);
            return false;
        }
    }

    // Export conversation with full loading
    async function exportConversation(format) {
        if (isExporting) {
            alert('Export already in progress. Please wait...');
            return;
        }

        isExporting = true;

        try {
            debugLog(`Starting FULL export in ${format} format...`);

            // Show loading indicator
            showNotification('🔄 Loading full conversation...', 0);

            // Load full conversation first
            if (CONFIG.autoScroll) {
                await loadFullConversation();
                showNotification('📝 Extracting messages...', 3000);
            }

            // Extract messages
            const messages = getConversationData();

            if (messages.length === 0) {
                alert('No conversation content found! This might be a new chat or there could be a technical issue. Check the browser console for details.');
                return;
            }

            // Generate filename with message count
            const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
            const filename = `claude-FULL-conversation-${messages.length}msgs-${timestamp}.${format}`;

            let content, mimeType;

            switch (format) {
                case 'md':
                    content = formatAsMarkdown(messages);
                    mimeType = 'text/markdown';
                    break;
                case 'json':
                    content = formatAsJSON(messages);
                    mimeType = 'application/json';
                    break;
                default:
                    content = formatAsText(messages);
                    mimeType = 'text/plain';
            }

            const success = downloadFile(content, filename, mimeType);
            if (success) {
                hideMenu();
                showNotification(`✅ Exported ${messages.length} messages as ${format.toUpperCase()}`, 5000);
            }

        } catch (error) {
            console.error('Export failed:', error);
            alert(`Export failed: ${error.message}\n\nCheck the browser console for more details.`);
        } finally {
            isExporting = false;
        }
    }

    // Show notification with auto-hide
    function showNotification(message, duration = 3000) {
        // Remove existing notification
        const existing = document.getElementById('claude-export-notification');
        if (existing) existing.remove();

        const notification = document.createElement('div');
        notification.id = 'claude-export-notification';
        notification.textContent = message;
        notification.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: #4CAF50;
            color: white;
            padding: 12px 20px;
            border-radius: 8px;
            z-index: 10000;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            box-shadow: 0 4px 12px rgba(0,0,0,0.2);
            max-width: 300px;
        `;

        document.body.appendChild(notification);

        if (duration > 0) {
            setTimeout(() => {
                if (notification.parentElement) {
                    notification.parentElement.removeChild(notification);
                }
            }, duration);
        }
    }

    // Create export menu with better styling
    function createExportMenu() {
        const menu = document.createElement('div');
        menu.id = 'claude-export-menu';
        menu.style.cssText = `
            position: fixed;
            bottom: 70px;
            right: 10px;
            background: #ffffff;
            border: 2px solid #667eea;
            border-radius: 12px;
            box-shadow: 0 8px 25px rgba(0,0,0,0.2);
            padding: 12px;
            z-index: 1000;
            display: none;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            min-width: 200px;
            color: #333333 !important;
        `;

        // Add title
        const title = document.createElement('div');
        title.textContent = 'Export Full Conversation';
        title.style.cssText = `
            font-weight: 600;
            color: #667eea !important;
            margin-bottom: 8px;
            padding-bottom: 8px;
            border-bottom: 1px solid #eee;
            font-size: 14px;
        `;
        menu.appendChild(title);

        const formats = [
            { ext: 'md', name: 'Markdown', icon: '📝', desc: 'Great for docs' },
            { ext: 'txt', name: 'Plain Text', icon: '📄', desc: 'Universal format' },
            { ext: 'json', name: 'JSON Data', icon: '📊', desc: 'Structured data' }
        ];

        formats.forEach(format => {
            const button = document.createElement('button');
            button.innerHTML = `
                <div style="display: flex; align-items: center;">
                    <span style="font-size: 16px; margin-right: 8px;">${format.icon}</span>
                    <div>
                        <div style="font-weight: 500;">${format.name}</div>
                        <div style="font-size: 11px; color: #666;">${format.desc}</div>
                    </div>
                </div>
            `;
            button.style.cssText = `
                display: block;
                width: 100%;
                padding: 10px;
                margin: 4px 0;
                border: none;
                background: transparent;
                text-align: left;
                cursor: pointer;
                border-radius: 6px;
                font-size: 14px;
                color: #333333 !important;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
                transition: background-color 0.2s;
            `;

            button.onmouseover = () => {
                button.style.background = '#f8f9ff';
                button.style.color = '#000000 !important';
            };
            button.onmouseout = () => {
                button.style.background = 'transparent';
                button.style.color = '#333333 !important';
            };

            button.onclick = () => exportConversation(format.ext);
            menu.appendChild(button);
        });

        // Debug button
        if (CONFIG.debug) {
            const debugButton = document.createElement('button');
            debugButton.innerHTML = '🔍 Debug Info';
            debugButton.style.cssText = `
                display: block;
                width: 100%;
                padding: 8px 12px;
                margin: 8px 0 4px 0;
                border: none;
                background: transparent;
                text-align: left;
                cursor: pointer;
                border-radius: 4px;
                font-size: 13px;
                border-top: 1px solid #eee;
                color: #666 !important;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            `;

            debugButton.onclick = () => {
                const messages = getConversationData();
                console.log('Debug Info:', {
                    messagesFound: messages.length,
                    url: window.location.href,
                    sampleMessages: messages.slice(0, 3)
                });
                alert(`Found ${messages.length} messages. Check console for details.`);
                hideMenu();
            };
            menu.appendChild(debugButton);
        }

        return menu;
    }

    // Show/hide menu
    function toggleMenu() {
        const menu = document.getElementById('claude-export-menu');
        if (menu) {
            menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
        }
    }

    function hideMenu() {
        const menu = document.getElementById('claude-export-menu');
        if (menu) menu.style.display = 'none';
    }

    // Create enhanced export button
    function createExportButton() {
        const button = document.createElement('button');
        button.innerHTML = `📥 Export Full`;
        button.id = 'claude-export-button';
        button.style.cssText = `
            position: fixed;
            bottom: 10px;
            right: 10px;
            padding: 10px 16px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border: 2px solid rgba(255,255,255,0.3);
            color: white;
            text-align: center;
            display: inline-block;
            font-size: 14px;
            font-weight: 600;
            cursor: pointer;
            border-radius: 30px;
            z-index: 999;
            box-shadow: 0 4px 15px rgba(0,0,0,0.2);
            transition: all 0.3s ease;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
        `;

        button.onmouseover = () => {
            button.style.transform = 'translateY(-3px) scale(1.05)';
            button.style.boxShadow = '0 6px 20px rgba(0,0,0,0.3)';
        };

        button.onmouseout = () => {
            button.style.transform = 'translateY(0) scale(1)';
            button.style.boxShadow = '0 4px 15px rgba(0,0,0,0.2)';
        };

        button.onclick = toggleMenu;

        return button;
    }

    // Initialize the script
    function init() {
        debugLog('Initializing Enhanced Claude Export v2.1...');

        // Remove existing elements
        const existingButton = document.getElementById('claude-export-button');
        const existingMenu = document.getElementById('claude-export-menu');
        if (existingButton) existingButton.remove();
        if (existingMenu) existingMenu.remove();

        // Create UI elements
        const button = createExportButton();
        const menu = createExportMenu();

        document.body.appendChild(button);
        document.body.appendChild(menu);

        // Close menu when clicking outside
        document.addEventListener('click', function(e) {
            if (!e.target.closest('#claude-export-menu') &&
                !e.target.closest('#claude-export-button')) {
                hideMenu();
            }
        });

        debugLog('Enhanced Claude Export v2.1 initialized successfully!');
        console.log('%c✅ Enhanced Claude Export v2.1 Ready!', 'color: green; font-weight: bold;');
        console.log('%cThis version will auto-scroll to load your FULL conversation before export.', 'color: blue;');
    }

    // Wait for page to be ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        setTimeout(init, 1000);
    }

    // Re-initialize on navigation
    let lastUrl = location.href;
    new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            setTimeout(init, 2000);
        }
    }).observe(document, { subtree: true, childList: true });

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址