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