Claude.ai Chat Exporter (Updated)

Adds "Export Chat" button to Claude.ai (current as of Sonnet 3.7)

  1. // ==UserScript==
  2. // @name Claude.ai Chat Exporter (Updated)
  3. // @description Adds "Export Chat" button to Claude.ai (current as of Sonnet 3.7)
  4. // @version 1.1
  5. // @author Merlin McKean
  6. // @namespace
  7. // @match https://claude.ai/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_download
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. const API_BASE_URL = 'https://claude.ai/api';
  17.  
  18. // Updated function to make API requests with new headers
  19. function apiRequest(method, endpoint, data = null) {
  20. return new Promise((resolve, reject) => {
  21. GM_xmlhttpRequest({
  22. method: method,
  23. url: `${API_BASE_URL}${endpoint}`,
  24. headers: {
  25. 'Content-Type': 'application/json',
  26. 'Accept': 'application/json',
  27. // Add any required authentication headers here
  28. },
  29. data: data ? JSON.stringify(data) : null,
  30. onload: (response) => {
  31. if (response.status >= 200 && response.status < 300) {
  32. resolve(JSON.parse(response.responseText));
  33. } else {
  34. reject(new Error(`API request failed with status ${response.status}`));
  35. }
  36. },
  37. onerror: (error) => {
  38. reject(error);
  39. },
  40. });
  41. });
  42. }
  43.  
  44. // Function to get the organization ID - updated for new API
  45. async function getOrganizationId() {
  46. try {
  47. const organizations = await apiRequest('GET', '/organizations');
  48. return organizations[0]?.uuid;
  49. } catch (error) {
  50. console.error('Error getting organization ID:', error);
  51. throw error;
  52. }
  53. }
  54.  
  55. // Updated function to get conversation history
  56. async function getConversationHistory(orgId, chatId) {
  57. return await apiRequest('GET', `/organizations/${orgId}/chat_conversations/${chatId}`);
  58. }
  59.  
  60. // Function to download data as a file
  61. function downloadData(data, filename, format) {
  62. let content = '';
  63. if (format === 'json') {
  64. content = JSON.stringify(data, null, 2);
  65. } else if (format === 'txt') {
  66. content = convertToTxtFormat(data);
  67. }
  68.  
  69. const blob = new Blob([content], { type: 'text/plain' });
  70. const url = URL.createObjectURL(blob);
  71. const a = document.createElement('a');
  72. a.href = url;
  73. a.download = filename;
  74. a.style.display = 'none';
  75. document.body.appendChild(a);
  76. a.click();
  77.  
  78. setTimeout(() => {
  79. document.body.removeChild(a);
  80. URL.revokeObjectURL(url);
  81. }, 100);
  82. }
  83.  
  84. // Updated function to convert conversation data to TXT format
  85. function convertToTxtFormat(data) {
  86. let txtContent = `Chat Title: ${data.name}\nDate: ${new Date().toISOString()}\n\n`;
  87. data.chat_messages.forEach((message) => {
  88. const sender = message.sender === 'human' ? 'User' : 'Claude';
  89. txtContent += `${sender}:\n${message.text}\n\n`;
  90. });
  91. return txtContent.trim();
  92. }
  93.  
  94. // Updated function to create a modern-styled button
  95. function createButton(text, onClick) {
  96. const button = document.createElement('button');
  97. button.textContent = text;
  98. button.style.cssText = `
  99. position: fixed;
  100. bottom: 80px;
  101. right: 20px;
  102. padding: 10px 20px;
  103. background-color: #4a5568;
  104. color: white;
  105. border: none;
  106. border-radius: 6px;
  107. cursor: pointer;
  108. font-size: 14px;
  109. z-index: 9999;
  110. font-family: system-ui, -apple-system, sans-serif;
  111. transition: all 0.2s;
  112. box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  113. `;
  114.  
  115. button.addEventListener('mouseover', () => {
  116. button.style.backgroundColor = '#2d3748';
  117. });
  118.  
  119. button.addEventListener('mouseout', () => {
  120. button.style.backgroundColor = '#4a5568';
  121. });
  122.  
  123. button.addEventListener('click', onClick);
  124. document.body.appendChild(button);
  125. return button;
  126. }
  127.  
  128. // Updated function to initialize the export functionality
  129. async function initExportFunctionality() {
  130. const currentUrl = window.location.href;
  131. const chatIdMatch = currentUrl.match(/\/chat\/([a-f0-9-]+)/);
  132.  
  133. if (chatIdMatch) {
  134. const chatId = chatIdMatch[1];
  135. try {
  136. const orgId = await getOrganizationId();
  137. const button = createButton('Export Chat', async () => {
  138. const format = prompt('Enter export format (json or txt):', 'json');
  139. if (format === 'json' || format === 'txt') {
  140. try {
  141. const chatData = await getConversationHistory(orgId, chatId);
  142. const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  143. const filename = `claude-chat_${timestamp}.${format}`;
  144. downloadData(chatData, filename, format);
  145. alert(`Chat exported successfully in ${format.toUpperCase()} format!`);
  146. } catch (error) {
  147. alert('Error exporting chat. Please try again.');
  148. console.error('Export error:', error);
  149. }
  150. } else {
  151. alert('Invalid format. Please enter either "json" or "txt".');
  152. }
  153. });
  154. } catch (error) {
  155. console.error('Error initializing export functionality:', error);
  156. }
  157. }
  158. }
  159.  
  160. // Function to observe URL changes
  161. function observeUrlChanges() {
  162. let lastUrl = location.href;
  163. new MutationObserver(() => {
  164. const url = location.href;
  165. if (url !== lastUrl) {
  166. lastUrl = url;
  167. initExportFunctionality();
  168. }
  169. }).observe(document, { subtree: true, childList: true });
  170. }
  171.  
  172. // Initialize the script when the page is ready
  173. function init() {
  174. initExportFunctionality();
  175. observeUrlChanges();
  176. }
  177.  
  178. // Start the script when the DOM is fully loaded
  179. if (document.readyState === 'complete') {
  180. init();
  181. } else {
  182. window.addEventListener('load', init);
  183. }
  184. })();