Last active
April 16, 2023 21:22
-
-
Save spion/667e720554b80e70e5fa008234e34cf0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name ChatGPT save to Markdown button | |
// @author spion, avosirenfal | |
// @description Adds an export button for exporting the doc to markdown | |
// @namespace chatgpt | |
// @version 1.0.0 | |
// @match https://chat.openai.com/* | |
// @grant none | |
// @run-at document-start | |
// ==/UserScript== | |
// Inspired by https://github.com/avosirenfal/chatgpt-export/blob/main/chatgpt-export.user.js | |
(function () { | |
'use strict'; | |
const turndownModule = import('https://cdn.skypack.dev/turndown'); | |
const Characters = { | |
Me: 'User', | |
Gpt: 'ChatGPT' | |
} | |
const callback = function (_mutationsList, _observer) { | |
const nav = document.querySelector("nav"); | |
if (nav.querySelector("#custom_save_button")) { | |
return; | |
} | |
const el = nav.lastElementChild.cloneNode(false); | |
el.id = 'custom_save_button'; | |
el.innerHTML = "Export Markdown"; | |
el.onclick = async function () { | |
const turndown = (await turndownModule).default; | |
const td = new turndown(); | |
td.addRule('fencedCodeBlock', { | |
filter: function (node, options) { | |
return ( | |
// options.codeBlockStyle === 'fenced' && | |
node.nodeName === 'PRE' && | |
node.querySelector('code.hljs') != null | |
) | |
}, | |
replacement: function (content, node, options) { | |
let codeBlock = node.querySelector('code.hljs'); | |
var className = codeBlock.getAttribute('class') || '' | |
var language = (className.match(/language-(\S+)/) || [null, ''])[1] | |
return `\n\n${'```'}${language}\n${codeBlock.textContent}\n${'```'}\n\n`; | |
} | |
}) | |
const html2markdown = html => td.turndown(html); | |
const markdownLink = document.createElement('a'); | |
let firstConversationItem = null; | |
let conversation = `# ${document.title}\n\n` | |
let currentCharacter = Characters.Me; | |
for (let item of Array.from(document.querySelectorAll('div:has(img) + div .items-start'))) { | |
firstConversationItem = item; | |
conversation += `### ${currentCharacter}\n\n` | |
if (item.querySelector('.markdown')) { | |
let gptHtml = item.querySelector('.markdown').innerHTML; | |
let gptMarkdown = html2markdown(gptHtml); | |
// Remove "Copy code" lines. | |
// Todo: convert small code blocks to larger code blocks with language | |
gptMarkdown = gptMarkdown.split("\n").filter(l => !/^[a-z]*Copy code$/.test(l)).join("\n"); | |
conversation += gptMarkdown; | |
} | |
else { | |
conversation += item.innerText; | |
} | |
conversation += "\n\n"; | |
if (currentCharacter == Characters.Me) { | |
currentCharacter = Characters.Gpt | |
} | |
else { | |
currentCharacter = Characters.Me | |
} | |
} | |
if (firstConversationItem === null) { | |
alert("Failed to get text for the items. The conversation is empty, or the CSS selector is no longer correct"); | |
return; | |
} | |
markdownLink.href = URL.createObjectURL(new Blob([conversation], { | |
type: 'text/markdown' | |
})); | |
markdownLink.download = `GPT Conversation - ${document.title}.md`; | |
document.body.appendChild(markdownLink); | |
markdownLink.click(); | |
document.body.removeChild(markdownLink); | |
URL.revokeObjectURL(markdownLink.href); | |
} | |
nav.appendChild(el); | |
}; | |
window.addEventListener('load', (event) => { | |
const observer = new MutationObserver(callback); | |
observer.observe(document.querySelector('body'), { attributes: true, childList: true, subtree: true }); | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment