// ==UserScript==
// @name Claude Chat Exporter
// @namespace lugia19.com
// @match https://claude.ai/*
// @version 2.0.0
// @author lugia19
// @license GPLv3
// @description Allows exporting chat conversations from claude.ai.
// @grant none
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
function getConversationId() {
const match = window.location.pathname.match(/\/chat\/([^/?]+)/);
return match ? match[1] : null;
}
function createExportButton() {
const button = document.createElement('button');
button.className = `inline-flex items-center justify-center relative shrink-0 ring-offset-2 ring-offset-bg-300
ring-accent-main-100 focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none
disabled:opacity-50 disabled:shadow-none disabled:drop-shadow-none text-text-200 border-transparent
transition-colors font-styrene active:bg-bg-400 hover:bg-bg-500/40 hover:text-text-100 h-9 w-9
rounded-md active:scale-95 shrink-0`;
button.innerHTML = ``;
// Add tooltip wrapper div
const tooltipWrapper = document.createElement('div');
tooltipWrapper.setAttribute('data-radix-popper-content-wrapper', '');
tooltipWrapper.style.cssText = `
position: fixed;
left: 0px;
top: 0px;
min-width: max-content;
--radix-popper-transform-origin: 50% 0px;
z-index: 50;
display: none;
`;
// Add tooltip content
tooltipWrapper.innerHTML = `
Export chatlog
Export chatlog
`;
// Add hover events
button.addEventListener('mouseenter', () => {
tooltipWrapper.style.display = 'block';
const rect = button.getBoundingClientRect();
const tooltipRect = tooltipWrapper.getBoundingClientRect();
const centerX = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
tooltipWrapper.style.transform = `translate(${centerX}px, ${rect.bottom + 5}px)`;
});
button.addEventListener('mouseleave', () => {
tooltipWrapper.style.display = 'none';
});
button.onclick = async () => {
// Show format selection modal
const format = await showFormatModal();
if (!format) return;
const messages = await getMessages();
const conversationId = getConversationId();
const filename = `Claude_export_${conversationId}.${format}`;
const content = formatExport(messages, format);
downloadFile(filename, content);
};
// Add tooltip to document
document.body.appendChild(tooltipWrapper);
return button;
}
async function showFormatModal() {
// Create and show a modal similar to Claude's style
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
modal.innerHTML = `
Export Format
`;
document.body.appendChild(modal);
return new Promise((resolve) => {
const select = modal.querySelector('select');
modal.querySelector('#cancelExport').onclick = () => {
modal.remove();
resolve(null);
};
modal.querySelector('#confirmExport').onclick = () => {
const format = select.value;
modal.remove();
resolve(format);
};
modal.onclick = (e) => {
if (e.target === modal) {
modal.remove();
resolve(null);
}
};
});
}
function getOrgId() {
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'lastActiveOrg') {
return value;
}
}
throw new Error('Could not find organization ID');
}
async function getMessages() {
const conversationId = getConversationId();
if (!conversationId) {
throw new Error('Not in a conversation');
}
const orgId = getOrgId();
const response = await fetch(`/api/organizations/${orgId}/chat_conversations/${conversationId}?tree=False&rendering_mode=messages&render_all_tools=true`);
const conversationData = await response.json();
const messages = [];
for (const message of conversationData.chat_messages) {
let messageContent = [];
for (const content of message.content) {
if (content.text) {
messageContent.push(content.text);
}
if (content.input?.code) {
messageContent.push(content.input.code);
}
if (content.content?.text) {
messageContent.push(content.content.text);
}
}
messages.push({
role: message.role === 'human' ? 'user' : 'assistant',
content: messageContent.join(' ')
});
}
return messages;
}
function formatExport(messages, format) {
switch (format) {
case 'txt':
return messages.map(msg => {
const role = msg.role === 'user' ? 'User' : 'Assistant';
return `[${role}]\n${msg.content}\n`;
}).join('\n');
case 'jsonl':
return messages.map(JSON.stringify).join('\n');
default:
throw new Error(`Unsupported format: ${format}`);
}
}
function downloadFile(filename, content) {
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}
function initialize() {
// Try to add the button immediately
tryAddButton();
// Also check every 5 seconds
setInterval(tryAddButton, 5000);
}
function tryAddButton() {
const container = document.querySelector('.right-3 .right-4 .hidden');
if (!container || container.querySelector('.export-button')) {
return; // Either container not found or button already exists
}
const exportButton = createExportButton();
exportButton.classList.add('export-button'); // Add class to check for existence
container.appendChild(exportButton);
}
initialize();
})();