🚀Export ChatGPT Chat as JSON or Markdown🚀

Newest userscript to export ChatGPT chat as JSON or Markdown. 📚All formulas 🧮 and tables📊 are fully preserved ✔️

// ==UserScript==
// @name         🚀Export ChatGPT Chat as JSON or Markdown🚀
// @namespace    ChatGPT Mods
// @version      1.0.3
// @author       ChatGPT Mods
// @description  Newest userscript to export ChatGPT chat as JSON or Markdown. 📚All formulas 🧮 and tables📊 are fully preserved ✔️
// @license      MIT
// @icon         data:image/svg+xml;base64,PHN2ZyBmaWxsPSJjdXJyZW50Q29sb3IiIGZpbGwtcnVsZT0iZXZlbm9kZCIgaGVpZ2h0PSIxZW0iIHN0eWxlPSJmbGV4Om5vbmU7bGluZS1oZWlnaHQ6MSIgdmlld0JveD0iMCAwIDI0IDI0IiB3aWR0aD0iMWVtIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjx0aXRsZT5PcGVuQUk8L3RpdGxlPjxwYXRoIGQ9Ik0yMS41NSAxMC4wMDRhNS40MTYgNS40MTYgMCAwMC0uNDc4LTQuNTAxYy0xLjIxNy0yLjA5LTMuNjYyLTMuMTY2LTYuMDUtMi42NkE1LjU5IDUuNTkgMCAwMDEwLjgzMSAxQzguMzkuOTk1IDYuMjI0IDIuNTQ2IDUuNDczIDQuODM4QTUuNTUzIDUuNTUzIDAgMDAxLjc2IDcuNDk2YTUuNDg3IDUuNDg3IDAgMDAuNjkxIDYuNSA1LjQxNiA1LjQxNiAwIDAwLjQ3NyA0LjUwMmMxLjIxNyAyLjA5IDMuNjYyIDMuMTY1IDYuMDUgMi42NkE1LjU4NiA1LjU4NiAwIDAwMTMuMTY4IDIzYzIuNDQzLjAwNiA0LjYxLTEuNTQ2IDUuMzYxLTMuODRhNS41NTMgNS41NTMgMCAwMDMuNzE1LTIuNjYgNS40ODggNS40ODggMCAwMC0uNjkzLTYuNDk3di4wMDF6bS04LjM4MSAxMS41NThhNC4xOTkgNC4xOTkgMCAwMS0yLjY3NS0uOTU0Yy4wMzQtLjAxOC4wOTMtLjA1LjEzMi0uMDc0bDQuNDQtMi41M2EuNzEuNzEgMCAwMC4zNjQtLjYyM3YtNi4xNzZsMS44NzcgMS4wNjljLjAyLjAxLjAzMy4wMjkuMDM2LjA1djUuMTE1Yy0uMDAzIDIuMjc0LTEuODcgNC4xMTgtNC4xNzQgNC4xMjN6TTQuMTkyIDE3Ljc4YTQuMDU5IDQuMDU5IDAgMDEtLjQ5OC0yLjc2M2MuMDMyLjAyLjA5LjA1NS4xMzEuMDc4bDQuNDQgMi41M2MuMjI1LjEzLjUwNC4xMy43MyAwbDUuNDItMy4wODh2Mi4xMzhhLjA2OC4wNjggMCAwMS0uMDI3LjA1N0w5LjkgMTkuMjg4Yy0xLjk5OSAxLjEzNi00LjU1Mi40Ni01LjcwNy0xLjUxaC0uMDAxek0zLjAyMyA4LjIxNkE0LjE1IDQuMTUgMCAwMTUuMTk4IDYuNDFsLS4wMDIuMTUxdjUuMDZhLjcxMS43MTEgMCAwMC4zNjQuNjI0bDUuNDIgMy4wODctMS44NzYgMS4wN2EuMDY3LjA2NyAwIDAxLS4wNjMuMDA1bC00LjQ4OS0yLjU1OWMtMS45OTUtMS4xNC0yLjY3OS0zLjY1OC0xLjUzLTUuNjNoLjAwMXptMTUuNDE3IDMuNTRsLTUuNDItMy4wODhMMTQuODk2IDcuNmEuMDY3LjA2NyAwIDAxLjA2My0uMDA2bDQuNDg5IDIuNTU3YzEuOTk4IDEuMTQgMi42ODMgMy42NjIgMS41MjkgNS42MzNhNC4xNjMgNC4xNjMgMCAwMS0yLjE3NCAxLjgwN1YxMi4zOGEuNzEuNzEgMCAwMC0uMzYzLS42MjN6bTEuODY3LTIuNzczYTYuMDQgNi4wNCAwIDAwLS4xMzItLjA3OGwtNC40NC0yLjUzYS43MzEuNzMxIDAgMDAtLjcyOSAwbC01LjQyIDMuMDg4VjcuMzI1YS4wNjguMDY4IDAgMDEuMDI3LS4wNTdMMTQuMSA0LjcxM2MyLTEuMTM3IDQuNTU1LS40NiA1LjcwNyAxLjUxMy40ODcuODMzLjY2NCAxLjgwOS40OTkgMi43NTdoLjAwMXptLTExLjc0MSAzLjgxbC0xLjg3Ny0xLjA2OGEuMDY1LjA2NSAwIDAxLS4wMzYtLjA1MVY2LjU1OWMuMDAxLTIuMjc3IDEuODczLTQuMTIyIDQuMTgxLTQuMTIuOTc2IDAgMS45Mi4zMzggMi42NzEuOTU0LS4wMzQuMDE4LS4wOTIuMDUtLjEzMS4wNzNsLTQuNDQgMi41M2EuNzEuNzEgMCAwMC0uMzY1LjYyM2wtLjAwMyA2LjE3M3YuMDAyem0xLjAyLTIuMTY4TDEyIDkuMjVsMi40MTQgMS4zNzV2Mi43NUwxMiAxNC43NWwtMi40MTUtMS4zNzV2LTIuNzV6Ij48L3BhdGg+PC9zdmc+
// @match        https://chatgpt.com/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.prod.js
// @grant        GM_download
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

(function (vue) {
  'use strict';

  const GLOBAL_MESSAGE_OPEN_AI_TOKEN_GOT = "GLOBAL_MESSAGE_OPEN_AI_TOKEN_GOT";
  const DOM_ID_HEADER_TOOL = "DOM_ID_HEADER_TOOL";
  const DOM_ID_GLOBAL_LAYER = "DOM_ID_GLOBAL_LAYER";
  var _GM_download = (() => typeof GM_download != "undefined" ? GM_download : void 0)();
  var _unsafeWindow = (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)();
  class FetchInterceptorManager {
    requestInterceptors = [];
    responseInterceptors = [];
    originalFetch;
    constructor() {
      this.originalFetch = _unsafeWindow.fetch.bind(_unsafeWindow);
      this.patchFetch();
    }
    patchFetch() {
      _unsafeWindow.fetch = async (input, init) => {
        const newInput = typeof input === "string" ? input : input instanceof URL ? input.toString() : input;
        let context = { input: newInput, init };
        for (const interceptor of this.requestInterceptors) {
          context = await interceptor(context.input, context.init);
        }
        const response = await this.originalFetch(context.input, context.init);
        let finalResponse = response;
        for (const interceptor of this.responseInterceptors) {
          finalResponse = await interceptor(finalResponse, context);
        }
        return finalResponse;
      };
    }
    useRequest(interceptor) {
      this.requestInterceptors.push(interceptor);
    }
    useResponse(interceptor) {
      this.responseInterceptors.push(interceptor);
    }
    ejectAll() {
      this.requestInterceptors = [];
      this.responseInterceptors = [];
      _unsafeWindow.fetch = this.originalFetch;
    }
  }
  const FetchInterceptor = new FetchInterceptorManager();
  bootstrap$1();
  function bootstrap$1() {
    FetchInterceptor.useRequest(async (input, init) => {
      try {
        const a = document.createElement("a");
        a.href = typeof input === "string" ? input : input.url;
        const url = a.href;
        console.log(url);
        const regexp = "/backend-api/(?!edge$)";
        if (regexp && new RegExp(regexp).test(url)) {
          let authorization = "";
          if (input instanceof Request) {
            authorization = input.headers.get("Authorization") || "";
          } else if (init?.headers) {
            authorization = init.headers.Authorization || "";
          }
          if (authorization) {
            window.postMessage({
              type: GLOBAL_MESSAGE_OPEN_AI_TOKEN_GOT,
              data: {
                authorization
              }
            });
          }
        }
      } catch {
      }
      return { input, init };
    });
  }
  vue.ref(false);
  vue.ref(false);
  vue.ref(false);
  vue.ref(false);
  vue.ref(false);
  vue.ref(false);
  const overlayContainer = vue.ref();
  function initGlobal(container) {
    overlayContainer.value = container;
  }
  const configStore = vue.ref({
    data: {
      chatGpt: {
        conversationIdRegExp: "/c/(?!WEB:)([0-9a-zA-Z-]+)",
        authRelatedPathRegExp: "/backend-api/(?!edge$)",
        apiConversationContent: "/backend-api/conversation/{{id}}"
      },
      injectTarget: {
        headerTool: '//*[@id="conversation-header-actions"]'
      }
    }
  });
  const userInfoStore = vue.ref({
    auth: {
      openaiAuth: ""
    }
  });
  function simpleTemplate(template, data) {
    if (!data) {
      return template;
    } else {
      return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
        return key in data ? data[key] : "";
      });
    }
  }
  function getConversationId(url) {
    const pathname = location.pathname;
    const match = pathname.match(new RegExp(configStore.value.data.chatGpt.conversationIdRegExp));
    if (match && match.length === 2) {
      return match[1];
    }
    return null;
  }
  function getElementsByXPath(xpath, contextNode = document, rootNode = document) {
    const snapshots = rootNode.evaluate(xpath, contextNode, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    if (snapshots.snapshotLength > 0) {
      const elements = [];
      for (let i = 0; i < snapshots.snapshotLength; i++) {
        const item = snapshots.snapshotItem(i);
        if (item) {
          elements.push(item);
        }
      }
      return elements;
    }
    return null;
  }
  function getElementByXPath(xpath, contextNode = document, rootNode = document) {
    const elements = getElementsByXPath(xpath, contextNode, rootNode);
    if (elements) {
      return elements[0];
    }
    return null;
  }
  function textToBase64Url(text, mimeType) {
    const encoder = new TextEncoder();
    const encoded = encoder.encode(text);
    const base64 = btoa(String.fromCharCode(...encoded));
    return `data:${mimeType};base64,${base64}`;
  }
  navigator.userAgent.toLocaleLowerCase().includes("mac");
  const _export_sfc = (sfc, props) => {
    const target = sfc.__vccOpts || sfc;
    for (const [key, val] of props) {
      target[key] = val;
    }
    return target;
  };
  const _sfc_main$1 = {};
  function _sfc_render(_ctx, _cache) {
    return null;
  }
  const GlobalOverlay = _export_sfc(_sfc_main$1, [["render", _sfc_render]]);
  async function request(method, path, params, signal) {
    if (!userInfoStore.value.auth.openaiAuth) {
      throw new Error("openAI not login");
    }
    let body = null;
    const headers = {
      authorization: userInfoStore.value.auth.openaiAuth
    };
    const response = await fetch(path, {
      signal,
      headers,
      method,
      body,
      mode: "cors",
      credentials: "include"
    });
    if (response.ok) {
      return response.json();
    } else {
      throw new Error(response.statusText);
    }
  }
  async function getConversationContent(id) {
    const result = await request("GET", simpleTemplate(configStore.value.data.chatGpt.apiConversationContent, { id }));
    const mapping = {};
    for (const [key, value] of Object.entries(result.mapping)) {
      if (value.message && value.message.content) {
        mapping[key] = {
          role: value.message.author?.role,
          type: value.message.content.content_type,
          content: value.message.content.parts?.filter((item) => typeof item === "string").join(""),
          recipient: value.message.recipient,
          parentId: value.parent
        };
      }
    }
    return {
      title: result.title,
      mapping,
      currentNode: result.current_node
    };
  }
  const _hoisted_1 = {
    key: 0,
    class: "flex gap-2 text-sm items-center h-full"
  };
  const _hoisted_2 = { key: 0 };
  const _hoisted_3 = { key: 0 };
  const _hoisted_4 = { key: 1 };
  const _hoisted_5 = { key: 0 };
  const _hoisted_6 = { key: 1 };
  const _hoisted_7 = {
    key: 1,
    class: "text-sm flex items-center h-full text-red-500"
  };
  const _sfc_main = vue.defineComponent({
    __name: "HeaderTool",
    setup(__props) {
      const isMac = navigator.userAgent.includes("Macintosh");
      const downloadingJson = vue.ref(false);
      const downloadingMarkdown = vue.ref(false);
      const counter = vue.ref(JSON.parse(localStorage.getItem("oai/apps/counter") || JSON.stringify({
        count: 0,
        date: ( new Date()).toDateString()
      })));
      if (counter.value.date !== ( new Date()).toDateString()) {
        counter.value = {
          count: 0,
          date: ( new Date()).toDateString()
        };
        localStorage.setItem("oai/apps/counter", JSON.stringify(counter.value));
      }
      vue.watch(counter, (value) => {
        localStorage.setItem("oai/apps/counter", JSON.stringify(value));
      }, {
        deep: true
      });
      function isLimited() {
        const today = ( new Date()).toDateString();
        if (counter.value.date !== today) {
          counter.value = {
            count: 0,
            date: today
          };
        }
        if (counter.value.count > 1) {
          return true;
        } else {
          counter.value.count++;
          return false;
        }
      }
      async function saveAsJson() {
        if (isLimited()) {
          return;
        }
        downloadingJson.value = true;
        const conversationData = await getConversationData();
        downloadingJson.value = false;
        if (conversationData) {
          const lastMessageId = conversationData.currentNode;
          let currentMessage = conversationData.mapping[lastMessageId];
          const result = [];
          while (currentMessage) {
            if (currentMessage.role && currentMessage.type && currentMessage.content && ["user", "assistant"].includes(currentMessage.role) && ["text", "multimodal_text"].includes(currentMessage.type) && currentMessage.recipient === "all") {
              result.push({
                role: currentMessage.role,
                content: currentMessage.content
              });
            }
            currentMessage = conversationData.mapping[currentMessage.parentId || ""];
          }
          result.reverse();
          const base64Url = textToBase64Url(JSON.stringify({
            title: conversationData.title,
            items: result
          }, null, 2), "application/json");
          _GM_download(base64Url, `${conversationData.title}.json`);
        }
      }
      async function saveAsMarkdown() {
        if (isLimited()) {
          return;
        }
        downloadingMarkdown.value = true;
        const conversationData = await getConversationData();
        downloadingMarkdown.value = false;
        if (conversationData) {
          const result = [];
          const lastMessageId = conversationData.currentNode;
          let currentMessage = conversationData.mapping[lastMessageId];
          while (currentMessage) {
            if (currentMessage.role && currentMessage.type && currentMessage.content && ["user", "assistant"].includes(currentMessage.role) && ["text", "multimodal_text"].includes(currentMessage.type) && currentMessage.recipient === "all") {
              result.push(`# ${currentMessage.role}:

${currentMessage.content}`);
            }
            currentMessage = conversationData.mapping[currentMessage.parentId || ""];
          }
          result.reverse();
          const base64Url = textToBase64Url(result.join("\n\n"), "text/markdown");
          _GM_download(base64Url, `${conversationData.title}.md.txt`);
        }
      }
      async function getConversationData() {
        const conversationId = getConversationId();
        if (conversationId) {
          return await getConversationContent(conversationId);
        }
      }
      return (_ctx, _cache) => {
        return vue.unref(userInfoStore).auth.openaiAuth ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [
          counter.value.count > 1 ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_2, [..._cache[0] || (_cache[0] = [
            vue.createTextVNode("You have reached today's limit. ", -1),
            vue.createElementVNode("a", {
              target: "_blank",
              class: "text-red-500 underline",
              href: "https://chromewebstore.google.com/detail/lmiigijnefpkjcenfbinhdpafehaddag?utm_source=userscript"
            }, [
              vue.createTextVNode("Get more for "),
              vue.createElementVNode("strong", null, "free🎉")
            ], -1)
          ])])) : (vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: 1 }, [
            vue.createElementVNode("button", {
              class: "rounded-2xl border border-gray-200 px-2 py-1 hover:bg-gray-100",
              onClick: saveAsJson
            }, [
              downloadingJson.value ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_3, "Downloading...")) : (vue.openBlock(), vue.createElementBlock("span", _hoisted_4, "Export JSON"))
            ]),
            vue.createElementVNode("button", {
              class: "rounded-2xl border border-gray-200 px-2 py-1 hover:bg-gray-100",
              onClick: saveAsMarkdown
            }, [
              downloadingMarkdown.value ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_5, "Downloading...")) : (vue.openBlock(), vue.createElementBlock("span", _hoisted_6, "Export Markdown"))
            ])
          ], 64))
        ])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_7, " Make a Hard Reload using " + vue.toDisplayString(vue.unref(isMac) ? "⇧⌘R" : "Ctrl+Shift+R") + " to activate the script ", 1));
      };
    }
  });
  const observerHandlers = [];
  waitForDomReady().then(() => {
    const observer = new MutationObserver((mutations, observer2) => {
      for (const handler of observerHandlers) {
        handler(mutations, observer2);
      }
    });
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
    if (localStorage.getItem("oai/apps/auth")) {
      userInfoStore.value.auth.openaiAuth = localStorage.getItem("oai/apps/auth");
    }
    bootstrap();
  });
  window.addEventListener("message", messageHandler);
  function messageHandler(e) {
    if (e.data.type === GLOBAL_MESSAGE_OPEN_AI_TOKEN_GOT) {
      userInfoStore.value.auth.openaiAuth = e.data.data.authorization;
      localStorage.setItem("oai/apps/auth", e.data.data.authorization);
    }
  }
  async function waitForDomReady() {
    return new Promise((resolve) => {
      console.log(document.readyState);
      if (document.readyState === "interactive" || document.readyState === "complete") {
        resolve();
      } else {
        window.addEventListener("DOMContentLoaded", () => resolve());
      }
    });
  }
  function bootstrap() {
    createGlobalLayer();
    const appBuilder = createAppBuilder();
    bootstrapHeaderTool(appBuilder);
  }
  function createAppBuilder() {
    function buildApp(cmp, id, props) {
      const container = document.createElement("div");
      container.id = id;
      const root = document.createElement("div");
      root.id = `app`;
      container.appendChild(root);
      const app = vue.createApp(cmp, props);
      return { app, root, container };
    }
    return buildApp;
  }
  function bootstrapHeaderTool(appBuilder) {
    const containerId = DOM_ID_HEADER_TOOL;
    let mounted = false;
    const { app, root, container } = appBuilder(_sfc_main, containerId);
    container.style.alignSelf = "stretch";
    root.style.height = "100%";
    function tryMount() {
      const pageHeader = getElementByXPath(configStore.value.data.injectTarget.headerTool);
      if (pageHeader instanceof HTMLElement) {
        if (!document.querySelector(`#${containerId}`) && getConversationId()) {
          pageHeader.prepend(container);
          if (!mounted) {
            mounted = true;
            app.mount(root);
          }
        }
      }
    }
    tryMount();
    observerHandlers.push(tryMount);
  }
  function createGlobalLayer() {
    const container = document.createElement("div");
    container.id = DOM_ID_GLOBAL_LAYER;
    const root = document.createElement("div");
    root.style.position = "fixed";
    root.style.left = "0px";
    root.style.top = "0px";
    root.id = `app`;
    const $globalLayer = document.createElement("div");
    $globalLayer.style.position = "fixed";
    $globalLayer.style.left = "0px";
    $globalLayer.style.top = "0px";
    $globalLayer.id = `container`;
    root.appendChild($globalLayer);
    initGlobal($globalLayer);
    container.appendChild(root);
    document.body.after(container);
    const globalLayerApp = vue.createApp(GlobalOverlay);
    globalLayerApp.mount($globalLayer);
  }

})(Vue);