/** * @name export.js * @version 0.1.5 * @url https://github.com/lencx/ChatGPT/tree/main/scripts/export.js */ async function exportInit() { const SELECTOR = 'main div.group'; const USER_INPUT_SELECTOR = 'div.empty\\:hidden'; const Format = { PNG: 'png', PDF: 'pdf', }; function processNode(node, replaceInUserInput = false) { let j = node.cloneNode(true); if (/dark\:bg-gray-800/.test(node.getAttribute('class'))) { j.innerHTML = `
${node.innerHTML}
`; } if (replaceInUserInput) { const userInputBlocks = j.querySelectorAll(USER_INPUT_SELECTOR); userInputBlocks.forEach((block) => { //For quicker testing use js fiddle: https://jsfiddle.net/xtraeme/x34ao9jp/13/ block.innerHTML = block.innerHTML .replace(/ |\u00A0/g, ' ') //Replace =C2=A0 (nbsp non-breaking space) /w breaking-space .replace(/\t/g, '    ') // Replace tab with 4 non-breaking spaces .replace(/^ +/gm, function (match) { return ' '.repeat(match.length); }) //Add =C2=A0 .replace(/\n/g, '
'); }); } return j.innerHTML; } async function exportMarkdown() { const allBlocks = document.querySelectorAll(SELECTOR); const nodes = Array.from(allBlocks); // blocks with leading spaces mess up ExportMD markdown conversion. ex/ // import package //becomes (spaces are moved to the front of the ''' line)): // '''Python import package //so we remove whitespace after tags and add
and/or \n allBlocks.forEach((block) => { block.innerHTML = block.innerHTML.replace(/(]*>)\s*/g, '$1\n'); // Add \n or
after opening code tag }); const updatedContent = nodes.map((i) => processNode(i, true)).join(''); const data = ExportMD.turndown(updatedContent); const { id, filename } = getName(); await invoke('save_file', { name: `notes/${id}.md`, content: data }); await invoke('download_list', { pathname: 'chat.notes.json', filename, id, dir: 'notes' }); } async function downloadThread({ as = Format.PNG } = {}) { const { startLoading, stopLoading } = new window.__LoadingMask('Exporting in progress...'); startLoading(); const elements = new Elements(); await elements.fixLocation(); const pixelRatio = window.devicePixelRatio; const minRatio = as === Format.PDF ? 2 : 2.5; window.devicePixelRatio = Math.max(pixelRatio, minRatio); html2canvas(elements.thread, { letterRendering: true, useCORS: true, }).then((canvas) => { elements.restoreLocation(); window.devicePixelRatio = pixelRatio; const imgData = canvas.toDataURL('image/png'); requestAnimationFrame(async () => { if (as === Format.PDF) { await handlePdf(imgData, canvas, pixelRatio); } else { await handleImg(imgData); } stopLoading(); }); }); } async function handleImg(imgData) { const binaryData = atob(imgData.split('base64,')[1]); const data = []; for (let i = 0; i < binaryData.length; i++) { data.push(binaryData.charCodeAt(i)); } const name = `ChatGPT_${formatDateTime()}.png`; await invoke('download_file', { name: name, blob: data }); } async function handlePdf(imgData, canvas, pixelRatio) { const { jsPDF } = window.jspdf; const orientation = canvas.width > canvas.height ? 'l' : 'p'; var pdf = new jsPDF(orientation, 'pt', [canvas.width / pixelRatio, canvas.height / pixelRatio]); var pdfWidth = pdf.internal.pageSize.getWidth(); var pdfHeight = pdf.internal.pageSize.getHeight(); pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight, '', 'FAST'); const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument()); const name = `ChatGPT_${formatDateTime()}.pdf`; await invoke('download_file', { name: name, blob: Array.from(new Uint8Array(data)) }); } class Elements { constructor() { this.init(); } init() { this.spacer = document.querySelector("main div[class*='h-'].flex-shrink-0"); this.thread = document.querySelector( "[class*='react-scroll-to-bottom']>[class*='react-scroll-to-bottom']>div", ); // fix: old chat https://github.com/lencx/ChatGPT/issues/185 if (!this.thread) { this.thread = document.querySelector('main .overflow-y-auto'); } // h-full overflow-y-auto this.positionForm = document.querySelector('form').parentNode; this.scroller = Array.from(document.querySelectorAll('[class*="react-scroll-to"]')).filter( (el) => el.classList.contains('h-full'), )[0]; // fix: old chat if (!this.scroller) { this.scroller = document.querySelector('main .overflow-y-auto'); } this.hiddens = Array.from(document.querySelectorAll('.overflow-hidden')); this.images = Array.from(document.querySelectorAll('img[srcset]')); this.chatImages = Array.from(document.querySelectorAll('main img[src]')); } async fixLocation() { this.hiddens.forEach((el) => { el.classList.remove('overflow-hidden'); }); this.spacer.style.display = 'none'; this.thread.style.maxWidth = '960px'; this.thread.style.marginInline = 'auto'; this.positionForm.style.display = 'none'; this.scroller.classList.remove('h-full'); this.scroller.style.minHeight = '100vh'; this.images.forEach((img) => { const srcset = img.getAttribute('srcset'); img.setAttribute('srcset_old', srcset); img.setAttribute('srcset', ''); }); const chatImagePromises = this.chatImages.map(async (img) => { const src = img.getAttribute('src'); if (!/^http/.test(src)) return; if (['fileserviceuploadsperm.blob.core.windows.net'].includes(new URL(src)?.host)) return; const data = await invoke('fetch_image', { url: src }); const blob = new Blob([new Uint8Array(data)], { type: 'image/png' }); img.src = URL.createObjectURL(blob); }); await Promise.all(chatImagePromises); document.body.style.lineHeight = '0.5'; } async restoreLocation() { this.hiddens.forEach((el) => { el.classList.add('overflow-hidden'); }); this.spacer.style.display = null; this.thread.style.maxWidth = null; this.thread.style.marginInline = null; this.positionForm.style.display = null; this.scroller.classList.add('h-full'); this.scroller.style.minHeight = null; } } function formatDateTime() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); const formattedDateTime = `${year}_${month}_${day}-${hours}${minutes}${seconds}`; return formattedDateTime; } // function sanitizeFilename(filename) { // if (!filename || filename === '') return ''; // // Replace whitespaces with underscores // let sanitizedFilename = filename.replace(/\s/g, '_'); // // Replace invalid filename characters with # // const invalidCharsRegex = /[<>:"/\\|?*\x00-\x1F]/g; // sanitizedFilename = sanitizedFilename.replace(invalidCharsRegex, '#'); // // Check for filenames ending with period or space (Windows) // if (sanitizedFilename && /[\s.]$/.test(sanitizedFilename)) { // sanitizedFilename = sanitizedFilename.slice(0, -1) + '#'; // } // //console.log(sanitizedFilename); // return sanitizedFilename; // } function btnInit() { const intervalId = setInterval(function () { const navActionArea = document.querySelector('nav .border-t > div'); const addArea = document.querySelector('#chatgpt-nav-action-area'); if (!navActionArea || addArea) return; const cloneNode = document.createElement('div'); cloneNode.id = 'chatgpt-nav-action-area'; cloneNode.classList = `${navActionArea.className} border-b border-white/20 mb-2 pb-2`; cloneNode.appendChild(addBtn('png')); cloneNode.appendChild(addBtn('pdf')); cloneNode.appendChild(addBtn('md')); cloneNode.appendChild(addBtn('refresh')); navActionArea.parentNode.insertBefore(cloneNode, navActionArea); clearInterval(intervalId); function debounce(func, wait) { let timeout; return function () { const context = this; const args = arguments; clearTimeout(timeout); timeout = setTimeout(() => { func.apply(context, args); }, wait); }; } const target = document.querySelector('nav'); const debouncedFunction = debounce(function () { btnInit(); }, 300); const observer = new MutationObserver(debouncedFunction); const config = { attributes: true, childList: true, characterData: true, subtree: true }; observer.observe(target, config); }, 1000); } function addBtn(type) { const btn = document.createElement('button'); btn.className = `btn dark:text-gray-500 hover:dark:text-gray-300`; btn.title = { png: 'Export PNG', pdf: 'Export PDF', md: 'Export Markdown', refresh: 'Refresh the Page', }[type]; btn.innerHTML = setIcon(type); btn.onclick = () => { const content = document.querySelector('main .group'); if (!content && type !== 'refresh') { alert('Please open a thread first.'); return; } switch (type) { case 'png': downloadThread(); break; case 'pdf': downloadThread({ as: 'pdf' }); break; case 'md': exportMarkdown(); break; case 'refresh': window.location.reload(); break; default: break; } }; return btn; } function setIcon(type) { return { png: ``, pdf: ``, md: ``, refresh: ``, }[type]; } function getName() { const id = window.crypto.getRandomValues(new Uint32Array(1))[0].toString(36); const name = document.querySelector('nav .overflow-y-auto a.hover\\:bg-gray-800')?.innerText?.trim() || ''; // clean_name = sanitizeFilename(name); return { id, filename: name ? name : id, pathname: 'chat.download.json', }; } btnInit(); } if (document.readyState === 'complete' || document.readyState === 'interactive') { exportInit(); } else { document.addEventListener('DOMContentLoaded', exportInit); }