├── .gitignore ├── bookmarklets └── export.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | 4 | # MacOS 5 | **/.DS_Store 6 | 7 | # IntelliJ IDEA 8 | .idea/* -------------------------------------------------------------------------------- /bookmarklets/export.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | /* v. 0.11, github.com/give-me/bookmarklets */ 3 | let dialog, events = [], extras = [], csp = false; 4 | // Get elements with a dialog and others 5 | switch (location.hostname) { 6 | case 'claude.ai': 7 | // Dialog 8 | dialog = document.querySelector('div[data-test-render-count]').parentElement; 9 | events = dialog.querySelectorAll('div[data-testid="user-message"], div[data-test-render-count]>div>div>div.font-claude-response'); 10 | // Open artifacts 11 | extras.push(document.querySelector('div.ease-out.w-full[class*="overflow-"]')); 12 | break; 13 | case 'chatgpt.com': 14 | // Dialog 15 | dialog = document.querySelector('article').parentElement; 16 | events = dialog.querySelectorAll('div[data-message-author-role]'); 17 | // Open canvas 18 | extras.push(document.querySelector('section.popover>section')); 19 | // CSP is strict 20 | csp = true; 21 | break; 22 | case 'grok.com': 23 | // Dialog 24 | dialog = document.querySelector('div#last-reply-container').parentElement; 25 | events = dialog.querySelectorAll('div.message-bubble'); 26 | // Open thoughts 27 | extras.push(document.querySelector('aside')); 28 | // CSP is strict 29 | csp = true; 30 | break; 31 | case 'gemini.google.com': 32 | // Dialog 33 | dialog = document.querySelector('#chat-history'); 34 | events = dialog.querySelectorAll('user-query-content, message-content'); 35 | // Open panels 36 | extras.push(document.querySelector('code-immersive-panel>div.container')); 37 | extras.push(document.querySelector('deep-research-immersive-panel>div.container')); 38 | extras.push(document.querySelector('extended-response-panel response-container')); 39 | // CSP is strict 40 | csp = true; 41 | break; 42 | default: 43 | return alert(location.hostname + ' is not supported'); 44 | } 45 | // Filter arrays 46 | events = [...events].filter(Boolean); 47 | extras = [...extras].filter(Boolean); 48 | // Log found elements for debugging 49 | console.group(`Found elements at ${location.hostname}:`); 50 | console.debug('dialog', dialog); 51 | console.debug('events', events); 52 | console.debug('extras', extras); 53 | console.groupEnd(); 54 | // Combine dialog and extras into an array 55 | let blocks = [dialog, ...extras]; 56 | // Get a timestamp for the filename 57 | let ts = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14); 58 | // Offer options to the user 59 | if (confirm('Confirm if you prefer to export PDF instead of text')) { 60 | if (csp || confirm('Confirm if the PDF should be searchable')) { 61 | // Clone elements to a temporary element 62 | let temp = document.createElement('div'); 63 | temp.id = 'id-' + Math.random().toString(36).slice(2, 9); 64 | blocks.forEach(el => temp.appendChild(el.cloneNode(true))); 65 | // Print the temporary element 66 | let style = document.createElement('style'); 67 | style.textContent = `@media print{body>*{display:none!important}#${temp.id}{display:flex!important;flex-direction:column}}`; 68 | document.head.appendChild(style); 69 | document.body.appendChild(temp); 70 | print(); 71 | // Clean up after printing 72 | setTimeout(() => { 73 | document.head.removeChild(style); 74 | document.body.removeChild(temp); 75 | }, 1000); 76 | } else { 77 | let script = document.createElement('script'); 78 | script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.12.1/html2pdf.bundle.min.js'; 79 | script.onload = function () { 80 | // Create a PDF from the first element 81 | let pdf = html2pdf().set({ 82 | margin: 5, 83 | filename: `${ts}.pdf`, 84 | html2canvas: {scale: 2, logging: false} 85 | }).from(blocks.shift()); 86 | // Add rest elements to the PDF 87 | blocks.forEach(el => pdf = pdf.toPdf().get('pdf').then(pdfObj => pdfObj.addPage()).from(el).toContainer().toCanvas().toPdf()); 88 | // Render the PDF 89 | pdf.save(); 90 | }; 91 | document.body.appendChild(script); 92 | } 93 | } else { 94 | // Generate text from dialog messages and extras 95 | let txt = events.map((e, i) => `# ${i % 2 ? 'AI' : 'Me'}:\n\n${e.innerText.trim()}\n\n`).join(''); 96 | txt += extras.map((e, i) => `# Extra ${i + 1}:\n\n${e.innerText.trim()}\n\n`).join(''); 97 | // extras.forEach((e, i) => txt += `# Extra ${i + 1}:\n\n${e.innerText.trim()}\n\n`); 98 | // Create a link to download the text file 99 | let href = URL.createObjectURL(new Blob(['\uFEFF', txt], {type: 'text/plain;charset=utf-8'})); 100 | let link = Object.assign(document.createElement('a'), {href: href, download: `${ts}.txt`}); 101 | // Click the link to start the download 102 | link.click(); 103 | // Clean up after downloading 104 | URL.revokeObjectURL(link.href); 105 | } 106 | })(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bookmarklet for Chatbots 2 | 3 | This bookmarklet allows you to export the content 4 | from [Claude](https://claude.ai/), [ChatGPT](https://chatgpt.com/), [Grok](https://grok.com/) 5 | and [Gemini](https://gemini.google.com/) to PDF or text with a single click. It's completely secure with no 6 | installations, data sharing with third parties, or extensions needed. Everything runs entirely in your browser. For 7 | chatbots that allow loading third-party libraries, the PDF can be searchable or not, depending on your choice. For other 8 | chatbots, the PDF will always be searchable. 9 | 10 | ## How to use 11 | 12 | 1. Add a new bookmark to your browser with any name (e.g. "Export to PDF or text") and the following code as the URL: 13 | 14 | ```javascript 15 | javascript:(function () { /* v. 0.11, github.com/give-me/bookmarklets */ let dialog, events = [], extras = [], csp = false; switch (location.hostname) { case 'claude.ai': dialog = document.querySelector('div[data-test-render-count]').parentElement; events = dialog.querySelectorAll('div[data-testid="user-message"], div[data-test-render-count]>div>div>div.font-claude-response'); extras.push(document.querySelector('div.ease-out.w-full[class*="overflow-"]')); break; case 'chatgpt.com': dialog = document.querySelector('article').parentElement; events = dialog.querySelectorAll('div[data-message-author-role]'); extras.push(document.querySelector('section.popover>section')); csp = true; break; case 'grok.com': dialog = document.querySelector('div#last-reply-container').parentElement; events = dialog.querySelectorAll('div.message-bubble'); extras.push(document.querySelector('aside')); csp = true; break; case 'gemini.google.com': dialog = document.querySelector('#chat-history'); events = dialog.querySelectorAll('user-query-content, message-content'); extras.push(document.querySelector('code-immersive-panel>div.container')); extras.push(document.querySelector('deep-research-immersive-panel>div.container')); extras.push(document.querySelector('extended-response-panel response-container')); csp = true; break; default: return alert(location.hostname + ' is not supported'); } events = [...events].filter(Boolean); extras = [...extras].filter(Boolean); console.group(`Found elements at ${location.hostname}:`); console.debug('dialog', dialog); console.debug('events', events); console.debug('extras', extras); console.groupEnd(); let blocks = [dialog, ...extras]; let ts = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14); if (confirm('Confirm if you prefer to export PDF instead of text')) { if (csp || confirm('Confirm if the PDF should be searchable')) { let temp = document.createElement('div'); temp.id = 'id-' + Math.random().toString(36).slice(2, 9); blocks.forEach(el => temp.appendChild(el.cloneNode(true))); let style = document.createElement('style'); style.textContent = `@media print{body>*{display:none!important}#${temp.id}{display:flex!important;flex-direction:column}}`; document.head.appendChild(style); document.body.appendChild(temp); print(); setTimeout(() => { document.head.removeChild(style); document.body.removeChild(temp); }, 1000); } else { let script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.12.1/html2pdf.bundle.min.js'; script.onload = function () { let pdf = html2pdf().set({ margin: 5, filename: `${ts}.pdf`, html2canvas: {scale: 2, logging: false} }).from(blocks.shift()); blocks.forEach(el => pdf = pdf.toPdf().get('pdf').then(pdfObj => pdfObj.addPage()).from(el).toContainer().toCanvas().toPdf()); pdf.save(); }; document.body.appendChild(script); } } else { let txt = events.map((e, i) => `# ${i % 2 ? 'AI' : 'Me'}:\n\n${e.innerText.trim()}\n\n`).join(''); txt += extras.map((e, i) => `# Extra ${i + 1}:\n\n${e.innerText.trim()}\n\n`).join(''); let href = URL.createObjectURL(new Blob(['\uFEFF', txt], {type: 'text/plain;charset=utf-8'})); let link = Object.assign(document.createElement('a'), {href: href, download: `${ts}.txt`}); link.click(); URL.revokeObjectURL(link.href); } })(); 16 | ``` 17 | 18 | 2. Open any conversation in [Claude](https://claude.ai/), [ChatGPT](https://chatgpt.com/), [Grok](https://grok.com/) 19 | or [Gemini](https://gemini.google.com/) and click on the bookmark. 20 | 3. Confirm if you prefer to export PDF instead of text. If PDF is chosen, confirm if the PDF should be searchable. Wait 21 | for the file to be generated. 22 | 23 | ## Under the hood 24 | 25 | If you choose a searchable PDF, this bookmarklet will create a temporary print-specific stylesheet and a temporary 26 | container, clone the content of the conversation and related data into this container. When printing, only this 27 | container will be displayed while all other page elements will be hidden. The native browser print function is used, 28 | which allows you to print directly or save as PDF. After printing, the temporary stylesheet and container will be 29 | automatically removed. 30 | 31 | If you choose a non-searchable PDF, this bookmarklet will load the [html2pdf](https://github.com/eKoopmans/html2pdf.js) 32 | library from the Cloudflare CDN to work locally in order to avoid sending any data to any server. The library will be 33 | used to convert the content of the conversation and related data to a PDF file (A4 format, portrait orientation, 5mm 34 | margins, and 2x scale). The filename will be generated based on the current date and time. 35 | 36 | ## About 37 | 38 | Source code is available [here](https://github.com/give-me/bookmarklets/blob/main/bookmarklets/export.js). When the user 39 | interfaces of chatbots change, this tool may need to be updated because the content of conversations and related data 40 | are found using CSS selectors. --------------------------------------------------------------------------------