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