Enhanced Claude Chat & Code Exporter 4.1

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

< 脚本Enhanced Claude Chat & Code Exporter 4.1的反馈

评价:好评 - 脚本运行良好

§
发布于:2025-05-01

Works good; though I made the mistake of clicking "export all" when there were 75 artifacts in the chat and got spammed with 75 popups asking where to save each file lol maybe there is a way to export as single zip when there are lots of artifacts? Anyways good work, feel free to steal my code from here: https://greasyfork.org/en/scripts/506542-export-claude-ai/code if you wanted to give the export buttons the ability to float or be moved around on the page.

§
发布于:2025-05-04

thanks for the link to your code. yes i tried a lot to export it as a single zip, was facing issues. might try again later when i have time. i feel the export button location is quiet decent as it is, non-intrusive ... anyhow will check next time. thanks agian.

§
发布于:2025-05-21

Cool you cool I make derivative of this?

§
发布于:2025-05-21

Eh, this 'kinda' worked. Maybe you could implement jszip library similar? Mine is POC not perfect; just tryna contribute :)


// ==UserScript==
// @name         Enhanced Claude Chat & Code Exporter 4.2 (with ZIP support)
// @namespace    http://tampermonkey.net/
// @version      4.2
// @description  Export Claude chat conversations and code artifacts into individual files or a ZIP archive
// @author       sharmanhall
// @match        https://claude.ai/chat/*
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// @grant        GM_download
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_download
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.0/jszip.min.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let zip = null;
    let useZip = false;

    function log(msg) {
        console.log(`[Claude Exporter] ${msg}`);
    }

    function generateTimestamp() {
        const now = new Date();
        return now.toISOString().replace(/[-:.]/g, '').slice(0, 15);
    }

    function sanitizeFileName(name) {
        return name.replace(/[\\/:*?"<>|]/g, '_').replace(/\s+/g, '_').slice(0, 100);
    }

    function getChatTitle() {
        const el = document.querySelector('.truncate.tracking-tight.font-normal.font-styrene');
        return el ? el.textContent.trim() : 'Claude_Conversation';
    }

    function downloadBlob(blob, filename) {
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        setTimeout(() => {
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }, 100);
    }

    function addToZip(filename, content) {
        if (!zip) zip = new JSZip();
        zip.file(filename, content);
    }

    async function exportConversation() {
        try {
            const timestamp = generateTimestamp();
            const chatTitle = sanitizeFileName(getChatTitle());
            const markdownFilename = `${timestamp}_00_${chatTitle}.md`;

            const artifactButtons = document.querySelectorAll('button[aria-label="Preview contents"]');
            useZip = artifactButtons.length > 2;

            zip = useZip ? new JSZip() : null;

            const markdown = extractConversationMarkdown();
            const markdownBlob = new Blob([markdown], { type: 'text/markdown' });

            if (useZip) {
                addToZip(markdownFilename, markdown);
            } else {
                downloadBlob(markdownBlob, markdownFilename);
            }

            for (let i = 0; i < artifactButtons.length; i++) {
                const button = artifactButtons[i];
                button.click();
                await new Promise(r => setTimeout(r, 1000));

                const codeBlock = document.querySelector('.code-block__code code') || document.querySelector('.code-block__code');
                if (!codeBlock) continue;

                const content = codeBlock.innerText;
                const artifactNumber = String(i + 1).padStart(2, '0');
                const filename = `${timestamp}_${artifactNumber}_artifact_${artifactNumber}.txt`;

                if (useZip) {
                    addToZip(filename, content);
                } else {
                    downloadBlob(new Blob([content], { type: 'text/plain' }), filename);
                }

                document.querySelector('header')?.click();
                await new Promise(r => setTimeout(r, 300));
            }

            if (useZip) {
                const zipBlob = await zip.generateAsync({ type: 'blob' });
                downloadBlob(zipBlob, `${timestamp}_${chatTitle}_Claude_Export.zip`);
            }

            alert(`Exported ${artifactButtons.length + 1} files ${useZip ? 'as ZIP' : ''}`);
        } catch (err) {
            console.error(`[Claude Exporter] Error during export:`, err);
            alert("Export failed. See console for details.");
        }
    }

    function extractConversationMarkdown() {
        let md = `# ${getChatTitle()}\n\n*Exported on: ${new Date().toLocaleString()}*\n\n`;
        const blocks = document.querySelectorAll('[data-test-render-count="1"]');

        blocks.forEach(block => {
            const user = block.querySelector('[data-testid="user-message"]');
            const ai = block.querySelector('.font-claude-message');

            if (user) {
                md += `## User\n\n${user.textContent.trim()}\n\n`;
            } else if (ai) {
                md += `## Claude\n\n`;
                ai.querySelectorAll('p, li, code, pre, blockquote, h1, h2, h3').forEach(el => {
                    if (el.tagName === 'P') md += `${el.textContent.trim()}\n\n`;
                    if (el.tagName === 'LI') md += `- ${el.textContent.trim()}\n`;
                    if (el.tagName === 'BLOCKQUOTE') md += `> ${el.textContent.trim()}\n\n`;
                    if (el.tagName.startsWith('H')) md += `${'#'.repeat(+el.tagName[1])} ${el.textContent.trim()}\n\n`;
                });
            }
        });

        return md;
    }

    function addExportButtons() {
        if (document.querySelector('.claude-export-button')) return;
        const toolbar = document.querySelector('.flex-row.items-center.gap-2');
        if (!toolbar) return;

        const container = document.createElement('div');
        container.className = 'claude-export-button';
        container.style.display = 'flex';
        container.style.gap = '8px';

        const btn = document.createElement('button');
        btn.textContent = '💾 Export All';
        btn.style.padding = '6px 12px';
        btn.style.background = '#4a6ee0';
        btn.style.color = '#fff';
        btn.style.borderRadius = '6px';
        btn.style.fontWeight = 'bold';
        btn.style.cursor = 'pointer';
        btn.onclick = exportConversation;

        container.appendChild(btn);
        toolbar.appendChild(container);
    }

    function init() {
        addExportButtons();
        const obs = new MutationObserver(addExportButtons);
        obs.observe(document.body, { childList: true, subtree: true });
        GM_registerMenuCommand("Export Claude Conversation + Artifacts", exportConversation);
    }

    init();
})();
§
发布于:2025-05-22

Thanks but I am not a expert in js. I just have a bit of experience with browser side js and appscript for very limited purpose related to some office work. I am primarily a hobbyist java developer. Right now I made the whole plugin entirely with claude, so you can guess, apart from instructions from my side, I am not the brain behind this. I will surely try this next time, but I cannot promise. I really value your contribution, this only makes open source and collaborative work happen. But I must be honest with you, I don't plan to work on this anytime soon.

发表回复

登录以发表回复。