Greasy Fork

Greasy Fork is available in English.

Claude Chat Downloader

Add download button to save Claude AI conversations in TXT, MD, or JSON format

目前为 2024-10-25 提交的版本,查看 最新版本

// ==UserScript==
// @name         Claude Chat Downloader
// @namespace    http://tampermonkey.net/
// @version      1.0 alpha
// @description  Add download button to save Claude AI conversations in TXT, MD, or JSON format
// @author       Papa Casper
// @license      MIT
// @homepage     https://papacasper.com
// @repository   https://github.com/PapaCasper
// @source       https://github.com/PapaCasper/claude-downloader
// @supportURL   https://github.com/PapaCasper/claude-downloader/issues
// @match        https://claude.ai/chat/*
// @match        https://claude.ai/chats/*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    const API_BASE_URL = 'https://claude.ai/api';

    const styles = `
        .claude-download-container {
            position: relative;
            display: inline-flex;
            align-items: center;
        }
        .claude-download-button {
            display: inline-flex;
            align-items: center;
            gap: 0.5rem;
            height: 2.25rem;
            padding: 0 0.75rem;
            border-radius: 0.375rem;
            font-size: 0.875rem;
            font-weight: 500;
            background-color: transparent;
            color: rgb(161, 161, 170);
            border: none;
            cursor: pointer;
            transition: all 0.15s ease;
            white-space: nowrap;
        }
        .claude-download-button:hover {
            background-color: rgb(39, 39, 42);
            color: rgb(250, 250, 250);
        }
        .claude-download-button svg {
            width: 1.25rem;
            height: 1.25rem;
        }
        .claude-dropdown {
            position: absolute;
            top: 100%;
            right: 0;
            margin-top: 0.5rem;
            background-color: rgb(24, 24, 27);
            border: 1px solid rgb(39, 39, 42);
            border-radius: 0.5rem;
            padding: 0.25rem;
            min-width: 10rem;
            display: none;
            flex-direction: column;
            gap: 0.125rem;
            z-index: 50;
            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
        }
        .claude-dropdown.show {
            display: flex;
        }
        .claude-dropdown-item {
            display: flex;
            align-items: center;
            gap: 0.75rem;
            padding: 0.625rem 0.75rem;
            border-radius: 0.375rem;
            color: rgb(250, 250, 250);
            background: transparent;
            border: none;
            cursor: pointer;
            font-size: 0.875rem;
            transition: all 0.15s ease;
            width: 100%;
            text-align: left;
            white-space: nowrap;
        }
        .claude-dropdown-item svg {
            width: 1rem;
            height: 1rem;
            color: rgb(161, 161, 170);
        }
        .claude-dropdown-item:hover {
            background-color: rgb(39, 39, 42);
        }
        .claude-mobile-menu {
            display: none;
            position: relative;
        }
        @media (max-width: 768px) {
            .claude-download-container {
                display: none;
            }
            .claude-mobile-menu {
                display: block;
            }
            .claude-mobile-menu .claude-dropdown {
                position: absolute;
                bottom: calc(100% + 0.5rem);
                right: 0;
                margin-top: 0;
            }
        }
        @media (prefers-color-scheme: light) {
            .claude-dropdown {
                background-color: rgb(250, 250, 250);
                border-color: rgb(228, 228, 231);
                box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
            }
            .claude-dropdown-item {
                color: rgb(24, 24, 27);
            }
            .claude-dropdown-item:hover {
                background-color: rgb(228, 228, 231);
            }
            .claude-dropdown-item svg {
                color: rgb(113, 113, 122);
            }
            .claude-download-button:hover {
                background-color: rgb(228, 228, 231);
                color: rgb(24, 24, 27);
            }
        }
    `;

    // Add styles
    const styleSheet = document.createElement('style');
    styleSheet.textContent = styles;
    document.head.appendChild(styleSheet);

    // API Request Function
    function apiRequest(method, endpoint, data = null, headers = {}) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: method,
                url: `${API_BASE_URL}${endpoint}`,
                headers: {
                    'Content-Type': 'application/json',
                    ...headers,
                },
                data: data ? JSON.stringify(data) : null,
                onload: (response) => {
                    if (response.status >= 200 && response.status < 300) {
                        resolve(JSON.parse(response.responseText));
                    } else {
                        reject(new Error(`API request failed with status ${response.status}`));
                    }
                },
                onerror: (error) => {
                    reject(error);
                },
            });
        });
    }

    // Get Organization ID
    async function getOrganizationId() {
        const organizations = await apiRequest('GET', '/organizations');
        return organizations[0].uuid;
    }

    // Get Conversation History
    async function getConversationHistory(orgId, chatId) {
        return await apiRequest('GET', `/organizations/${orgId}/chat_conversations/${chatId}`);
    }

    // Format conversion
    function convertToFormat(data, format) {
        if (format === 'json') {
            return JSON.stringify(data, null, 2);
        } else if (format === 'txt') {
            return data.chat_messages.map(message => {
                const sender = message.sender === 'human' ? 'User' : 'Claude';
                return `${sender}:\n${message.text}\n\n`;
            }).join('');
        } else if (format === 'md') {
            let content = `# Claude Chat Export\n\n`;
            content += `*Exported on ${new Date().toLocaleString()}*\n\n---\n\n`;

            data.chat_messages.forEach(message => {
                const sender = message.sender === 'human' ? 'User' : 'Claude';
                const text = message.text.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
                    return `\`\`\`${lang}\n${code.trim()}\`\`\`\n`;
                });
                content += `### ${sender}\n\n${text}\n\n---\n\n`;
            });

            return content;
        }
    }

    // Download Function
    async function downloadChat(format) {
        try {
            const orgId = await getOrganizationId();
            const chatId = window.location.pathname.split('/').pop();
            const chatData = await getConversationHistory(orgId, chatId);

            const content = convertToFormat(chatData, format);
            const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
            const filename = `claude-chat-${timestamp}.${format}`;

            const blob = new Blob([content], { type: 'text/plain' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = filename;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        } catch (error) {
            console.error('Error downloading chat:', error);
            alert('Error downloading chat. Please try again.');
        }
    }

    // Helper function to create dropdown menu
    function createDropdown() {
        const dropdown = document.createElement('div');
        dropdown.className = 'claude-dropdown';

        const formats = [
            { id: 'txt', label: 'Export as TXT' },
            { id: 'md', label: 'Export as MD' },
            { id: 'json', label: 'Export as JSON' }
        ];

        formats.forEach(format => {
            const item = document.createElement('button');
            item.className = 'claude-dropdown-item';
            item.innerHTML = `
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256">
                    <path d="M224,152v56a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V152a8,8,0,0,1,16,0v56H208V152a8,8,0,0,1,16,0ZM117.66,154.34a8,8,0,0,0,11.31,0l40-40a8,8,0,0,0-11.31-11.31L136,124.69V40a8,8,0,0,0-16,0v84.69L98.34,103a8,8,0,0,0-11.31,11.31Z"/>
                </svg>
                ${format.label}
            `;
            item.addEventListener('click', (e) => {
                e.stopPropagation();
                downloadChat(format.id);
                dropdown.classList.remove('show');
            });
            dropdown.appendChild(item);
        });

        return dropdown;
    }

    // Create and add download button
    function addDownloadButton() {
        if (document.querySelector('.claude-download-container')) return;

        // Create desktop version
        const container = document.createElement('div');
        container.className = 'claude-download-container';

        const button = document.createElement('button');
        button.className = 'claude-download-button';
        button.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 256 256">
                <path d="M224,152v56a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V152a8,8,0,0,1,16,0v56H208V152a8,8,0,0,1,16,0ZM117.66,154.34a8,8,0,0,0,11.31,0l40-40a8,8,0,0,0-11.31-11.31L136,124.69V40a8,8,0,0,0-16,0v84.69L98.34,103a8,8,0,0,0-11.31,11.31Z"/>
            </svg>
            Export
        `;

        const dropdown = createDropdown();
        container.appendChild(button);
        container.appendChild(dropdown);

        // Create mobile version
        const mobileMenu = document.createElement('div');
        mobileMenu.className = 'claude-mobile-menu';
        const mobileButton = button.cloneNode(true);
        const mobileDropdown = createDropdown();
        mobileMenu.appendChild(mobileButton);
        mobileMenu.appendChild(mobileDropdown);

        // Add click handlers
        function handleButtonClick(dropdownElement) {
            return (e) => {
                e.stopPropagation();
                // Close any other open dropdowns
                document.querySelectorAll('.claude-dropdown').forEach(d => {
                    if (d !== dropdownElement) d.classList.remove('show');
                });
                dropdownElement.classList.toggle('show');
            };
        }

        button.addEventListener('click', handleButtonClick(dropdown));
        mobileButton.addEventListener('click', handleButtonClick(mobileDropdown));

        // Close dropdown when clicking outside
        document.addEventListener('click', () => {
            document.querySelectorAll('.claude-dropdown').forEach(d => {
                d.classList.remove('show');
            });
        });

        // Insert desktop version
        const targetContainer = document.querySelector('.hidden.flex-row-reverse.gap-1\\.5.md\\:flex');
        if (targetContainer) {
            targetContainer.insertBefore(container, targetContainer.firstChild);
        }

        // Insert mobile version
        const mobileContainer = document.querySelector('.flex.items-center.md\\:hidden');
        if (mobileContainer) {
            mobileContainer.insertBefore(mobileMenu, mobileContainer.firstChild);
        }
    }

    // Observer setup
    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            if (mutation.addedNodes.length) {
                addDownloadButton();
            }
        }
    });

    function startObserver() {
        const targetDiv = document.querySelector('.right-3.flex.gap-2.md\\:absolute');
        if (targetDiv) {
            observer.observe(targetDiv, {
                childList: true,
                subtree: true
            });
            addDownloadButton();
        } else {
            setTimeout(startObserver, 500);
        }
    }

    startObserver();
})();