/** * @name export.js * @version 0.1.2 * @url https://github.com/lencx/ChatGPT/tree/main/scripts/export.js */ async function exportInit() { if (window.location.pathname === '/auth/login') return; const buttonOuterHTMLFallback = ``; removeButtons(); if (window.buttonsInterval) { clearInterval(window.buttonsInterval); } if (window.innerWidth < 767) return; const chatConf = (await invoke('get_app_conf')) || {}; window.buttonsInterval = setInterval(() => { const actionsArea = document.querySelector('form>div>div>div'); const hasBtn = document.querySelector('form>div>div>div button'); if (!actionsArea || !hasBtn) { return; } if (shouldAddButtons(actionsArea)) { let TryAgainButton = actionsArea.querySelector('button'); if (!TryAgainButton) { const parentNode = document.createElement('div'); parentNode.innerHTML = buttonOuterHTMLFallback; TryAgainButton = parentNode.querySelector('button'); } addActionsButtons(actionsArea, TryAgainButton, chatConf); } else if (shouldRemoveButtons()) { removeButtons(); } }, 1000); const Format = { PNG: 'png', PDF: 'pdf', }; function shouldRemoveButtons() { if (document.querySelector('form .text-2xl')) { return true; } return false; } function shouldAddButtons(actionsArea) { // first, check if there's a "Try Again" button and no other buttons const buttons = actionsArea.querySelectorAll('button'); const hasTryAgainButton = Array.from(buttons).some((button) => { return !/download-/.test(button.id); }); const stopBtn = buttons?.[0]?.innerText; if (/Stop generating/gi.test(stopBtn)) { return false; } if ( buttons.length === 2 && (/Regenerate response/gi.test(stopBtn) || buttons[1].innerText === '') ) { return true; } if (hasTryAgainButton && buttons.length === 1) { return true; } // otherwise, check if open screen is not visible const isOpenScreen = document.querySelector('h1.text-4xl'); if (isOpenScreen) { return false; } // check if the conversation is finished and there are no share buttons const finishedConversation = document.querySelector('form button>svg'); const hasShareButtons = actionsArea.querySelectorAll('button[share-ext]'); if (finishedConversation && !hasShareButtons.length) { return true; } return false; } function removeButtons() { const downloadPngButton = document.getElementById('download-png-button'); const downloadPdfButton = document.getElementById('download-pdf-button'); const downloadMdButton = document.getElementById('download-markdown-button'); const refreshButton = document.getElementById('refresh-page-button'); if (downloadPngButton) { downloadPngButton.remove(); } if (downloadPdfButton) { downloadPdfButton.remove(); } if (downloadPdfButton) { downloadMdButton.remove(); } if (refreshButton) { refreshButton.remove(); } } function addActionsButtons(actionsArea, TryAgainButton) { // Export markdown const exportMd = TryAgainButton.cloneNode(true); exportMd.id = 'download-markdown-button'; exportMd.setAttribute('share-ext', 'true'); exportMd.title = 'Export Markdown'; exportMd.innerHTML = setIcon('md'); exportMd.onclick = () => { exportMarkdown(); }; actionsArea.appendChild(exportMd); // Generate PNG const downloadPngButton = TryAgainButton.cloneNode(true); downloadPngButton.id = 'download-png-button'; downloadPngButton.setAttribute('share-ext', 'true'); downloadPngButton.title = 'Generate PNG'; downloadPngButton.innerHTML = setIcon('png'); downloadPngButton.onclick = () => { downloadThread(); }; actionsArea.appendChild(downloadPngButton); // Generate PDF const downloadPdfButton = TryAgainButton.cloneNode(true); downloadPdfButton.id = 'download-pdf-button'; downloadPdfButton.setAttribute('share-ext', 'true'); downloadPdfButton.title = 'Download PDF'; downloadPdfButton.innerHTML = setIcon('pdf'); downloadPdfButton.onclick = () => { downloadThread({ as: Format.PDF }); }; actionsArea.appendChild(downloadPdfButton); // Refresh const refreshButton = TryAgainButton.cloneNode(true); refreshButton.id = 'refresh-page-button'; refreshButton.title = 'Refresh the Page'; refreshButton.innerHTML = setIcon('refresh'); refreshButton.onclick = () => { window.location.reload(); }; actionsArea.appendChild(refreshButton); } async function exportMarkdown() { const content = Array.from(document.querySelectorAll('main div.group')) .map((i) => { let j = i.cloneNode(true); if (/dark\:bg-gray-800/.test(i.getAttribute('class'))) { j.innerHTML = `
${i.innerHTML}
`; } return j.innerHTML; }) .join(''); const data = ExportMD.turndown(content); 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; 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); } 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 setIcon(type) { return { png: ``, pdf: ``, md: ``, refresh: ``, }[type]; } 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 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() || ''; return { filename: name ? name : id, id, pathname: 'chat.download.json' }; } } window.addEventListener('resize', exportInit); if (document.readyState === 'complete' || document.readyState === 'interactive') { exportInit(); } else { document.addEventListener('DOMContentLoaded', exportInit); }