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 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

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