Export Claude chat conversations with code artifacts into individual files with timestamp prefixes
< 脚本Enhanced Claude Chat & Code Exporter 4.1的反馈
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.
Cool you cool I make derivative of this?
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();
})();
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.
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.