Claude.ai ShareGPT Exporter

Adds "Export Chat" buttons to Claude.ai

// ==UserScript==
// @name         Claude.ai ShareGPT Exporter
// @description  Adds "Export Chat" buttons to Claude.ai
// @version      1.0
// @author       EndlessReform
// @namespace    https://github.com/EndlessReform/claude-sharegpt-exporter
// @match        https://claude.ai/*
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

/*
NOTES:
- This project is a fork of GeoAnima's fork of "Export Claude.Ai" (https://github.com/TheAlanK/export-claude), licensed under the MIT license.
- The "Export All Chats" option can only be accessed from the https://claude.ai/chats URL.
*/

(function () {
  "use strict";

  const API_BASE_URL = "https://claude.ai/api";

  // Function to make API requests
  function apiRequest(method, endpoint, data = null, headers = {}) {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: method,
        url: `${API_BASE_URL}${endpoint}`,
        headers: {
          "Content-Type": "application/json",
          ...headers,
        },
        data: data ? JSON.stringify(data) : null,
        onload: (response) => {
          if (response.status >= 200 && response.status < 300) {
            resolve(JSON.parse(response.responseText));
          } else {
            reject(
              new Error(
                `API request failed with status ${response.status}: ${response.responseText}`
              )
            );
          }
        },
        onerror: (error) => {
          reject(error);
        },
      });
    });
  }

  // Function to get the organization ID
  async function getOrganizationId() {
    let orgId = GM_getValue("orgId");
    if (typeof orgId === "undefined") {
      const organizations = await apiRequest("GET", "/organizations");
      const new_id = organizations[0].uuid;
      GM_setValue("orgId", new_id);
      return new_id;
    } else {
      return orgId;
    }
  }

  // Function to get all conversations
  async function getAllConversations(orgId) {
    return await apiRequest(
      "GET",
      `/organizations/${orgId}/chat_conversations`
    );
  }

  // Function to get conversation history
  async function getConversationHistory(orgId, chatId) {
    return await apiRequest(
      "GET",
      `/organizations/${orgId}/chat_conversations/${chatId}`
    );
  }

  // Function to download data as a file
  function downloadData(data, filename) {
    return new Promise((resolve, reject) => {
      let content = JSON.stringify(data, null, 2);
      const blob = new Blob([content], { type: "text/plain" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = filename;
      a.style.display = "none";
      document.body.appendChild(a);
      a.click();
      setTimeout(() => {
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
        resolve();
      }, 100);
    });
  }

  function transformChatToConversation(input) {
    const { chat_messages, current_leaf_message_uuid } = input;

    // Map to store messages by their uuid for easy lookup
    const messagesMap = new Map();
    chat_messages.forEach((message) => messagesMap.set(message.uuid, message));

    // Traverse back from the leaf to the root
    let currentMessage = messagesMap.get(current_leaf_message_uuid);
    const conversation = [];

    while (
      currentMessage &&
      currentMessage.parent_message_uuid !==
        "00000000-0000-4000-8000-000000000000"
    ) {
      conversation.unshift({
        from:
          currentMessage.sender === "assistant" ? "gpt" : currentMessage.sender,
        value: currentMessage.text,
      });
      currentMessage = messagesMap.get(currentMessage.parent_message_uuid);
    }

    // Add the root message
    if (currentMessage) {
      conversation.unshift({
        from:
          currentMessage.sender === "assistant" ? "gpt" : currentMessage.sender,
        value: currentMessage.text,
      });
    }

    return { conversations: conversation };
  }

  // Function to export a single chat
  async function exportChat(orgId, chatId, showAlert = true) {
    try {
      const originalChatData = await getConversationHistory(orgId, chatId);
      const chatData = transformChatToConversation(originalChatData);
      const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
      const filename = `${originalChatData.name}_${timestamp}.json`;
      await downloadData(chatData, filename);
    } catch (error) {
      alert("Error exporting chat. Please try again later.");
    }
  }

  // Function to export all chats
  async function exportAllChats(format) {
    try {
      const orgId = await getOrganizationId();
      const conversations = await getAllConversations(orgId);
      for (const conversation of conversations) {
        await exportChat(orgId, conversation.uuid, format, false);
      }
    } catch (error) {
      console.error(error);
      alert("Error exporting all chats: see browser console for details");
    }
  }

  // Function to create a button
  function createButton(text, onClick) {
    const style = document.createElement("style");
    // Add the CSS rules to the style element
    style.innerHTML = `
          #gm-export {
            position: fixed;
            top: 10px;
            right: 100px;
            padding: 6px 12px;
            color: hsl(var(--text-400) / var(--tw-text-opacity));
            border-radius: 0.375rem;
            cursor: pointer;
            font-size: 16px;
            z-index: 9999;
            border: 1px solid hsl(var(--bg-400));
            box-sizing: border-box;
          }

          #gm-export:hover {
            background-color: hsl(var(--bg-400));
          }
        `;

    // Add the style element to the document head
    document.head.appendChild(style);

    const button = document.createElement("button");
    button.textContent = text;
    button.id = "gm-export";
    button.addEventListener("click", onClick);
    document.body.appendChild(button);
  }

  // Function to remove existing export buttons
  function removeExportButtons() {
    const existingButton = document.getElementById("gm-export");
    if (existingButton) {
      existingButton.parentNode.removeChild(existingButton);
    }
  }

  // Function to initialize the export functionality
  async function initExportFunctionality() {
    removeExportButtons();
    const currentUrl = window.location.href;
    if (currentUrl.includes("/chat/")) {
      const urlParts = currentUrl.split("/");
      const chatId = urlParts[urlParts.length - 1];
      const orgId = await getOrganizationId();
      createButton("Export Chat", async () => {
        await exportChat(orgId, chatId);
      });
    } else if (currentUrl.includes("/chats")) {
      createButton("Export All Chats", async () => {
        const format = prompt("Enter the export format (json or txt):", "json");
        if (format === "json" || format === "txt") {
          await exportAllChats(format);
        } else {
          alert('Invalid export format. Please enter either "json" or "txt".');
        }
      });
    }
  }

  // Function to observe changes in the URL
  function observeUrlChanges(callback) {
    let lastUrl = location.href;
    const observer = new MutationObserver(() => {
      const url = location.href;
      if (url !== lastUrl) {
        lastUrl = url;
        callback();
      }
    });
    const config = { subtree: true, childList: true };
    observer.observe(document, config);
  }

  // Function to observe changes in the DOM
  function observeDOMChanges(selector, callback) {
    const observer = new MutationObserver((mutations) => {
      const element = document.querySelector(selector);
      if (element) {
        if (document.readyState === "complete") {
          observer.disconnect();
          callback();
        }
      }
    });

    observer.observe(document.documentElement, {
      childList: true,
      subtree: true,
    });
  }

  // Function to initialize the script
  async function init() {
    await initExportFunctionality();
    // Observe URL changes and reinitialize export functionality
    observeUrlChanges(async () => {
      await initExportFunctionality();
    });
  }

  // Wait for the desired element to be present in the DOM before initializing the script
  observeDOMChanges(".grecaptcha-badge", init);
})();