Claude.ai ShareGPT Exporter

Adds "Export Chat" buttons to Claude.ai

  1. // ==UserScript==
  2. // @name Claude.ai ShareGPT Exporter
  3. // @description Adds "Export Chat" buttons to Claude.ai
  4. // @version 1.0
  5. // @author EndlessReform
  6. // @namespace https://github.com/EndlessReform/claude-sharegpt-exporter
  7. // @match https://claude.ai/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_download
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. /*
  16. NOTES:
  17. - This project is a fork of GeoAnima's fork of "Export Claude.Ai" (https://github.com/TheAlanK/export-claude), licensed under the MIT license.
  18. - The "Export All Chats" option can only be accessed from the https://claude.ai/chats URL.
  19. */
  20.  
  21. (function () {
  22. "use strict";
  23.  
  24. const API_BASE_URL = "https://claude.ai/api";
  25.  
  26. // Function to make API requests
  27. function apiRequest(method, endpoint, data = null, headers = {}) {
  28. return new Promise((resolve, reject) => {
  29. GM_xmlhttpRequest({
  30. method: method,
  31. url: `${API_BASE_URL}${endpoint}`,
  32. headers: {
  33. "Content-Type": "application/json",
  34. ...headers,
  35. },
  36. data: data ? JSON.stringify(data) : null,
  37. onload: (response) => {
  38. if (response.status >= 200 && response.status < 300) {
  39. resolve(JSON.parse(response.responseText));
  40. } else {
  41. reject(
  42. new Error(
  43. `API request failed with status ${response.status}: ${response.responseText}`
  44. )
  45. );
  46. }
  47. },
  48. onerror: (error) => {
  49. reject(error);
  50. },
  51. });
  52. });
  53. }
  54.  
  55. // Function to get the organization ID
  56. async function getOrganizationId() {
  57. let orgId = GM_getValue("orgId");
  58. if (typeof orgId === "undefined") {
  59. const organizations = await apiRequest("GET", "/organizations");
  60. const new_id = organizations[0].uuid;
  61. GM_setValue("orgId", new_id);
  62. return new_id;
  63. } else {
  64. return orgId;
  65. }
  66. }
  67.  
  68. // Function to get all conversations
  69. async function getAllConversations(orgId) {
  70. return await apiRequest(
  71. "GET",
  72. `/organizations/${orgId}/chat_conversations`
  73. );
  74. }
  75.  
  76. // Function to get conversation history
  77. async function getConversationHistory(orgId, chatId) {
  78. return await apiRequest(
  79. "GET",
  80. `/organizations/${orgId}/chat_conversations/${chatId}`
  81. );
  82. }
  83.  
  84. // Function to download data as a file
  85. function downloadData(data, filename) {
  86. return new Promise((resolve, reject) => {
  87. let content = JSON.stringify(data, null, 2);
  88. const blob = new Blob([content], { type: "text/plain" });
  89. const url = URL.createObjectURL(blob);
  90. const a = document.createElement("a");
  91. a.href = url;
  92. a.download = filename;
  93. a.style.display = "none";
  94. document.body.appendChild(a);
  95. a.click();
  96. setTimeout(() => {
  97. document.body.removeChild(a);
  98. URL.revokeObjectURL(url);
  99. resolve();
  100. }, 100);
  101. });
  102. }
  103.  
  104. function transformChatToConversation(input) {
  105. const { chat_messages, current_leaf_message_uuid } = input;
  106.  
  107. // Map to store messages by their uuid for easy lookup
  108. const messagesMap = new Map();
  109. chat_messages.forEach((message) => messagesMap.set(message.uuid, message));
  110.  
  111. // Traverse back from the leaf to the root
  112. let currentMessage = messagesMap.get(current_leaf_message_uuid);
  113. const conversation = [];
  114.  
  115. while (
  116. currentMessage &&
  117. currentMessage.parent_message_uuid !==
  118. "00000000-0000-4000-8000-000000000000"
  119. ) {
  120. conversation.unshift({
  121. from:
  122. currentMessage.sender === "assistant" ? "gpt" : currentMessage.sender,
  123. value: currentMessage.text,
  124. });
  125. currentMessage = messagesMap.get(currentMessage.parent_message_uuid);
  126. }
  127.  
  128. // Add the root message
  129. if (currentMessage) {
  130. conversation.unshift({
  131. from:
  132. currentMessage.sender === "assistant" ? "gpt" : currentMessage.sender,
  133. value: currentMessage.text,
  134. });
  135. }
  136.  
  137. return { conversations: conversation };
  138. }
  139.  
  140. // Function to export a single chat
  141. async function exportChat(orgId, chatId, showAlert = true) {
  142. try {
  143. const originalChatData = await getConversationHistory(orgId, chatId);
  144. const chatData = transformChatToConversation(originalChatData);
  145. const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
  146. const filename = `${originalChatData.name}_${timestamp}.json`;
  147. await downloadData(chatData, filename);
  148. } catch (error) {
  149. alert("Error exporting chat. Please try again later.");
  150. }
  151. }
  152.  
  153. // Function to export all chats
  154. async function exportAllChats(format) {
  155. try {
  156. const orgId = await getOrganizationId();
  157. const conversations = await getAllConversations(orgId);
  158. for (const conversation of conversations) {
  159. await exportChat(orgId, conversation.uuid, format, false);
  160. }
  161. } catch (error) {
  162. console.error(error);
  163. alert("Error exporting all chats: see browser console for details");
  164. }
  165. }
  166.  
  167. // Function to create a button
  168. function createButton(text, onClick) {
  169. const style = document.createElement("style");
  170. // Add the CSS rules to the style element
  171. style.innerHTML = `
  172. #gm-export {
  173. position: fixed;
  174. top: 10px;
  175. right: 100px;
  176. padding: 6px 12px;
  177. color: hsl(var(--text-400) / var(--tw-text-opacity));
  178. border-radius: 0.375rem;
  179. cursor: pointer;
  180. font-size: 16px;
  181. z-index: 9999;
  182. border: 1px solid hsl(var(--bg-400));
  183. box-sizing: border-box;
  184. }
  185.  
  186. #gm-export:hover {
  187. background-color: hsl(var(--bg-400));
  188. }
  189. `;
  190.  
  191. // Add the style element to the document head
  192. document.head.appendChild(style);
  193.  
  194. const button = document.createElement("button");
  195. button.textContent = text;
  196. button.id = "gm-export";
  197. button.addEventListener("click", onClick);
  198. document.body.appendChild(button);
  199. }
  200.  
  201. // Function to remove existing export buttons
  202. function removeExportButtons() {
  203. const existingButton = document.getElementById("gm-export");
  204. if (existingButton) {
  205. existingButton.parentNode.removeChild(existingButton);
  206. }
  207. }
  208.  
  209. // Function to initialize the export functionality
  210. async function initExportFunctionality() {
  211. removeExportButtons();
  212. const currentUrl = window.location.href;
  213. if (currentUrl.includes("/chat/")) {
  214. const urlParts = currentUrl.split("/");
  215. const chatId = urlParts[urlParts.length - 1];
  216. const orgId = await getOrganizationId();
  217. createButton("Export Chat", async () => {
  218. await exportChat(orgId, chatId);
  219. });
  220. } else if (currentUrl.includes("/chats")) {
  221. createButton("Export All Chats", async () => {
  222. const format = prompt("Enter the export format (json or txt):", "json");
  223. if (format === "json" || format === "txt") {
  224. await exportAllChats(format);
  225. } else {
  226. alert('Invalid export format. Please enter either "json" or "txt".');
  227. }
  228. });
  229. }
  230. }
  231.  
  232. // Function to observe changes in the URL
  233. function observeUrlChanges(callback) {
  234. let lastUrl = location.href;
  235. const observer = new MutationObserver(() => {
  236. const url = location.href;
  237. if (url !== lastUrl) {
  238. lastUrl = url;
  239. callback();
  240. }
  241. });
  242. const config = { subtree: true, childList: true };
  243. observer.observe(document, config);
  244. }
  245.  
  246. // Function to observe changes in the DOM
  247. function observeDOMChanges(selector, callback) {
  248. const observer = new MutationObserver((mutations) => {
  249. const element = document.querySelector(selector);
  250. if (element) {
  251. if (document.readyState === "complete") {
  252. observer.disconnect();
  253. callback();
  254. }
  255. }
  256. });
  257.  
  258. observer.observe(document.documentElement, {
  259. childList: true,
  260. subtree: true,
  261. });
  262. }
  263.  
  264. // Function to initialize the script
  265. async function init() {
  266. await initExportFunctionality();
  267. // Observe URL changes and reinitialize export functionality
  268. observeUrlChanges(async () => {
  269. await initExportFunctionality();
  270. });
  271. }
  272.  
  273. // Wait for the desired element to be present in the DOM before initializing the script
  274. observeDOMChanges(".grecaptcha-badge", init);
  275. })();