- // ==UserScript==
- // @name ChatGPT对话Markdown导出
- // @name:en ChatGPT to Markdown Exporter
- // @version 1.2
- // @description 导出ChatGPT和Grok网站上的对话为Markdown格式.
- // @description:en Export chat history from ChatGPT and Grok websites to Markdown format.
- // @author ChingyuanCheng //origin from: Marverlises
- // @license MIT
- // @match https://chatgpt.com/*
- // @match https://*.openai.com/*
- // @match https://grok.com/*
- // @grant none
- // @namespace https://gf.qytechs.cn/users/1435416
- // ==/UserScript==
-
- (function() {
- // Select chat elements based on the website
- function getConversationElements() {
- const currentUrl = window.location.href;
- if (currentUrl.includes("openai.com") || currentUrl.includes("chatgpt.com")) {
- return document.querySelectorAll('div.flex.flex-grow.flex-col.max-w-full');
- } else if (currentUrl.includes("grok.com")) {
- return document.querySelectorAll('div.message-bubble');
- }
- return [];
- }
-
- // Convert HTML to Markdown
- function htmlToMarkdown(html) {
- const parser = new DOMParser();
- const doc = parser.parseFromString(html, 'text/html');
-
- // Handle formulas
- doc.querySelectorAll('span.katex-html').forEach(element => element.remove());
- doc.querySelectorAll('mrow').forEach(mrow => mrow.remove());
- doc.querySelectorAll('annotation[encoding="application/x-tex"]').forEach(element => {
- if (element.closest('.katex-display')) {
- const latex = element.textContent;
- element.replaceWith(`\n$$\n${latex}\n$$\n`);
- } else {
- const latex = element.textContent;
- element.replaceWith(`$${latex}$`);
- }
- });
-
- // Bold text
- doc.querySelectorAll('strong, b').forEach(bold => {
- bold.parentNode.replaceChild(document.createTextNode(`**${bold.textContent}**`), bold);
- });
-
- // Italic text
- doc.querySelectorAll('em, i').forEach(italic => {
- italic.parentNode.replaceChild(document.createTextNode(`*${italic.textContent}*`), italic);
- });
-
- // Inline code
- doc.querySelectorAll('p code').forEach(code => {
- code.parentNode.replaceChild(document.createTextNode(`\`${code.textContent}\``), code);
- });
-
- // Links
- doc.querySelectorAll('a').forEach(link => {
- link.parentNode.replaceChild(document.createTextNode(`[${link.textContent}](${link.href})`), link);
- });
-
- // Images
- doc.querySelectorAll('img').forEach(img => {
- img.parentNode.replaceChild(document.createTextNode(``), img);
- });
-
- // Code blocks
- doc.querySelectorAll('pre').forEach(pre => {
- const codeType = pre.querySelector('div > div:first-child')?.textContent || '';
- const markdownCode = pre.querySelector('div > div:nth-child(3) > code')?.textContent || pre.textContent;
- pre.innerHTML = `\n\`\`\`${codeType}\n${markdownCode}\`\`\`\n`;
- });
-
- // Unordered lists
- doc.querySelectorAll('ul').forEach(ul => {
- let markdown = '';
- ul.querySelectorAll(':scope > li').forEach(li => {
- markdown += `- ${li.textContent.trim()}\n`;
- });
- ul.parentNode.replaceChild(document.createTextNode('\n' + markdown.trim()), ul);
- });
-
- // Ordered lists
- doc.querySelectorAll('ol').forEach(ol => {
- let markdown = '';
- ol.querySelectorAll(':scope > li').forEach((li, index) => {
- markdown += `${index + 1}. ${li.textContent.trim()}\n`;
- });
- ol.parentNode.replaceChild(document.createTextNode('\n' + markdown.trim()), ol);
- });
-
- // Headers
- for (let i = 1; i <= 6; i++) {
- doc.querySelectorAll(`h${i}`).forEach(header => {
- header.parentNode.replaceChild(document.createTextNode('\n' + `${'#'.repeat(i)} ${header.textContent}\n`), header);
- });
- }
-
- // Paragraphs
- doc.querySelectorAll('p').forEach(p => {
- p.parentNode.replaceChild(document.createTextNode('\n' + p.textContent + '\n'), p);
- });
-
- // Tables
- doc.querySelectorAll('table').forEach(table => {
- let markdown = '';
- table.querySelectorAll('thead tr').forEach(tr => {
- tr.querySelectorAll('th').forEach(th => {
- markdown += `| ${th.textContent} `;
- });
- markdown += '|\n';
- tr.querySelectorAll('th').forEach(() => {
- markdown += '| ---- ';
- });
- markdown += '|\n';
- });
- table.querySelectorAll('tbody tr').forEach(tr => {
- tr.querySelectorAll('td').forEach(td => {
- markdown += `| ${td.textContent} `;
- });
- markdown += '|\n';
- });
- table.parentNode.replaceChild(document.createTextNode('\n' + markdown.trim() + '\n'), table);
- });
-
- let markdown = doc.body.innerHTML.replace(/<[^>]*>/g, '');
- markdown = markdown.replaceAll(/- >/g, '- $\\gt$')
- .replaceAll(/>/g, '>')
- .replaceAll(/</g, '<')
- .replaceAll(/≥/g, '>=')
- .replaceAll(/≤/g, '<=')
- .replaceAll(/≠/g, '\\neq');
- return markdown.trim();
- }
-
- // Download content as a file
- function download(data, filename, type) {
- const file = new Blob([data], { type: type });
- const a = document.createElement('a');
- const url = URL.createObjectURL(file);
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- setTimeout(() => {
- document.body.removeChild(a);
- window.URL.revokeObjectURL(url);
- }, 0);
- }
-
- // Show export modal with Markdown content
- function showExportModal() {
- let markdownContent = "";
- const allElements = getConversationElements();
-
- for (let i = 0; i < allElements.length; i += 2) {
- if (!allElements[i + 1]) break;
- let userText = allElements[i].textContent.trim();
- let answerHtml = allElements[i + 1].innerHTML.trim();
-
- userText = htmlToMarkdown(userText);
- answerHtml = htmlToMarkdown(answerHtml);
-
- const isGrok = window.location.href.includes("grok.com");
- markdownContent += `\n# User Question\n${userText}\n# ${isGrok ? 'Grok' : 'ChatGPT'}\n${answerHtml}`;
- }
- markdownContent = markdownContent.replace(/&/g, '&');
-
- if (!markdownContent) {
- alert("No conversation content found.");
- return;
- }
-
- // Create modal
- const modal = document.createElement('div');
- modal.id = 'markdown-modal';
- Object.assign(modal.style, {
- position: 'fixed',
- top: '0',
- left: '0',
- width: '100%',
- height: '100%',
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- zIndex: '1000'
- });
-
- const modalContent = document.createElement('div');
- Object.assign(modalContent.style, {
- backgroundColor: '#fff',
- color: '#000',
- padding: '20px',
- borderRadius: '8px',
- width: '50%',
- height: '80%',
- display: 'flex',
- flexDirection: 'column',
- boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
- overflow: 'hidden'
- });
-
- const textarea = document.createElement('textarea');
- textarea.value = markdownContent;
- Object.assign(textarea.style, {
- flex: '1',
- resize: 'none',
- width: '100%',
- padding: '10px',
- fontSize: '14px',
- fontFamily: 'monospace',
- marginBottom: '10px',
- boxSizing: 'border-box',
- color: '#000',
- backgroundColor: '#f9f9f9',
- border: '1px solid #ccc',
- borderRadius: '4px'
- });
- textarea.setAttribute('readonly', true);
-
- const buttonContainer = document.createElement('div');
- Object.assign(buttonContainer.style, {
- display: 'flex',
- justifyContent: 'flex-end'
- });
-
- const copyButton = document.createElement('button');
- copyButton.textContent = 'Copy';
- Object.assign(copyButton.style, {
- padding: '8px 16px',
- fontSize: '14px',
- cursor: 'pointer',
- backgroundColor: '#28A745',
- color: '#fff',
- border: 'none',
- borderRadius: '4px',
- marginRight: '10px'
- });
-
- const downloadButton = document.createElement('button');
- downloadButton.textContent = 'Download';
- Object.assign(downloadButton.style, {
- padding: '8px 16px',
- fontSize: '14px',
- cursor: 'pointer',
- backgroundColor: '#007BFF',
- color: '#fff',
- border: 'none',
- borderRadius: '4px',
- marginRight: '10px'
- });
-
- const closeButton = document.createElement('button');
- closeButton.textContent = 'Close';
- Object.assign(closeButton.style, {
- padding: '8px 16px',
- fontSize: '14px',
- cursor: 'pointer',
- backgroundColor: '#DC3545',
- color: '#fff',
- border: 'none',
- borderRadius: '4px'
- });
-
- buttonContainer.appendChild(copyButton);
- buttonContainer.appendChild(downloadButton);
- buttonContainer.appendChild(closeButton);
- modalContent.appendChild(textarea);
- modalContent.appendChild(buttonContainer);
- modal.appendChild(modalContent);
- document.body.appendChild(modal);
-
- // Event listeners for buttons
- copyButton.addEventListener('click', () => {
- textarea.select();
- navigator.clipboard.writeText(textarea.value)
- .then(() => {
- copyButton.textContent = 'Copied';
- setTimeout(() => copyButton.textContent = 'Copy', 2000);
- })
- .catch(err => console.error('Copy failed', err));
- });
-
- downloadButton.addEventListener('click', () => {
- download(markdownContent, 'chat-export.md', 'text/markdown');
- });
-
- closeButton.addEventListener('click', () => {
- document.body.removeChild(modal);
- });
-
- // Close modal with Escape key or click outside
- const escListener = (e) => {
- if (e.key === 'Escape' && document.getElementById('markdown-modal')) {
- document.body.removeChild(modal);
- document.removeEventListener('keydown', escListener);
- }
- };
- document.addEventListener('keydown', escListener);
-
- modal.addEventListener('click', (e) => {
- if (e.target === modal) {
- document.body.removeChild(modal);
- document.removeEventListener('keydown', escListener);
- }
- });
- }
-
- // Create the export button on the page
- function createExportButton() {
- const exportButton = document.createElement('button');
- exportButton.textContent = 'Export Chat';
- exportButton.id = 'export-chat';
- const styles = {
- position: 'fixed',
- height: '36px',
- top: '10px',
- right: '35%',
- zIndex: '10000',
- padding: '10px',
- backgroundColor: '#000000',
- color: 'white',
- border: 'none',
- borderRadius: '5px',
- cursor: 'pointer',
- textAlign: 'center',
- lineHeight: '16px'
- };
- Object.assign(exportButton.style, styles);
- document.body.appendChild(exportButton);
- exportButton.addEventListener('click', showExportModal);
- }
-
- // Initialize button and periodically check its presence
- createExportButton();
- setInterval(() => {
- if (!document.getElementById('export-chat')) {
- createExportButton();
- }
- }, 1000);
- })();