/** * @name export.js * @version 0.1.4 * @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 formArea = document.querySelector('form>div>div'); const textarea = formArea.querySelector('div textarea'); const textareaDiv = formArea.querySelector('div div.absolute'); const hasBtn = formArea.querySelector('div button'); const actionsArea = document.querySelector('form>div>div>div'); if (!formArea || !actionsArea || (textarea && textareaDiv) || !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); } const SELECTOR = 'main div.group'; const USER_INPUT_SELECTOR = 'div.empty\\:hidden'; 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, '
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 content = nodes.map(i => processNode(i)).join('');
const updatedContent = nodes.map(i => processNode(i, true)).join('');
const data = ExportMD.turndown(updatedContent);
const { id, filename } = getName();
final_filename = `${filename}`; //`${filename}_${id}`;
//await invoke('save_file', { name: `notes/${final_filename}_raw.txt`, content: content });
await invoke('save_file', { name: `notes/${id}.md`, content: data });
await invoke('download_list', { pathname: 'chat.notes.json', final_filename, id, dir: 'notes' });
//await invoke('download_list', { pathname: 'chat.notes.json', final_filename, final_filename, 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 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 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 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 { 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);
}