Skip to content

Instantly share code, notes, and snippets.

@minanagehsalalma
Last active February 14, 2026 16:14
Show Gist options
  • Select an option

  • Save minanagehsalalma/d2b15c616721c4de05be1b4416785655 to your computer and use it in GitHub Desktop.

Select an option

Save minanagehsalalma/d2b15c616721c4de05be1b4416785655 to your computer and use it in GitHub Desktop.
A tampermonkey script to export a selected ChatGPT message to proper pdf
// ==UserScript==
// @name ChatGPT Message PDF Downloader — Arabic/RTL (no @require)
// @namespace https://github.com/
// @version 10.0
// @description Export any ChatGPT message to a centered A4 PDF, with full Arabic/RTL support and dark theme. Normal click = selectable text via print; Shift+click = one-click image PDF.
// @author You
// @match https://chatgpt.com/*
// @match https://chat.openai.com/*
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @connect cdn.jsdelivr.net
// @connect cdnjs.cloudflare.com
// @connect unpkg.com
// @connect fonts.googleapis.com
// @connect fonts.gstatic.com
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ---------- A4 geometry ----------
const MM_TO_PX = 96 / 25.4;
const A4_W_PX = Math.round(210 * MM_TO_PX); // ~794 px
const A4_H_PX = Math.round(297 * MM_TO_PX); // ~1123 px
const PAD_MM = 15;
const PAD_PX = Math.round(PAD_MM * MM_TO_PX);
// Canvas safety:
// Many browsers cap canvas width/height to ~16384px. We avoid that by rendering page-by-page when needed.
const SAFE_MAX_CANVAS_DIM = 16384;
// ---------- CDNs (multiple fallbacks) ----------
const H2C_URLS = [
'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js',
'https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js'
];
const JSPDF_URLS = [
'https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js',
'https://unpkg.com/jspdf@2.5.1/dist/jspdf.umd.min.js'
];
// ---------- small utils ----------
const containsArabic = (s) => /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(s || '');
const sanitizeFilename = (name) => (name || '').replace(/[\/\\?%*:|"<>]/g, '_').trim() || 'ChatGPT';
function ensureArabicFonts() {
if (document.getElementById('cgptpdf-ar-fonts')) return;
const link = document.createElement('link');
link.id = 'cgptpdf-ar-fonts';
link.rel = 'stylesheet';
link.href = 'https://fonts.googleapis.com/css2?family=Noto+Kufi+Arabic:wght@400;700&family=Noto+Naskh+Arabic:wght@400;700&display=swap';
document.head.appendChild(link);
}
async function waitForFonts(doc = document) {
if (!doc.fonts) return;
try {
await Promise.allSettled([
doc.fonts.load('16px "Noto Naskh Arabic"'),
doc.fonts.load('16px "Noto Kufi Arabic"')
]);
await doc.fonts.ready;
} catch {/* ignore */}
}
// ---------- CSP-safe text fetch (GM_xhr first, then fetch) ----------
function requestText(url) {
// Tampermonkey / Violentmonkey:
if (typeof GM_xmlhttpRequest === 'function') {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
responseType: 'text',
anonymous: true,
onload: (r) => {
const status = r.status || 0;
if (status >= 200 && status < 300) resolve(r.responseText || '');
else reject(new Error('HTTP ' + status));
},
onerror: () => reject(new Error('network_error')),
ontimeout: () => reject(new Error('timeout'))
});
});
}
// Greasemonkey 4+ / some managers:
if (typeof GM !== 'undefined' && GM && typeof GM.xmlHttpRequest === 'function') {
return GM.xmlHttpRequest({ method: 'GET', url, responseType: 'text', anonymous: true })
.then((r) => {
const status = r.status || 0;
if (status >= 200 && status < 300) return r.responseText || '';
throw new Error('HTTP ' + status);
});
}
// Last resort:
return fetch(url, { cache: 'no-store', credentials: 'omit' })
.then((res) => {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.text();
});
}
async function fetchEval(url) {
const code = await requestText(url);
// Evaluate in this userscript context:
(0, eval)(code + '\n//# sourceURL=' + url);
}
async function loadLibSequence(urls, testFn) {
if (testFn()) return true;
for (const url of urls) {
try {
await fetchEval(url);
if (testFn()) return true;
} catch { /* try next */ }
}
return !!testFn();
}
async function ensureLibsOrNull() {
const gotH2C = await loadLibSequence(H2C_URLS, () =>
(typeof html2canvas !== 'undefined') || (window && window.html2canvas)
);
const gotJSPDF = await loadLibSequence(JSPDF_URLS, () =>
(window && ((window.jspdf && window.jspdf.jsPDF) || window.jsPDF))
);
return (gotH2C && gotJSPDF) ? {
html2canvas: window.html2canvas || html2canvas,
JsPDF: (window.jspdf && window.jspdf.jsPDF) || window.jsPDF
} : null;
}
// ---------- extraction & cleaning ----------
function extractAndCleanContent(turnElement) {
const messageContent = turnElement.querySelector('[data-message-author-role]') || turnElement;
const clone = messageContent.cloneNode(true);
clone.querySelectorAll('*').forEach(el => {
const tag = el.tagName.toLowerCase();
// strip UI and scripts
if (['button', 'script', 'style', 'svg'].includes(tag) || el.getAttribute('role') === 'button') {
el.remove(); return;
}
// keep safe attrs (bidi + links + images)
const allowed = new Set(['href', 'dir', 'lang', 'target', 'rel', 'src', 'alt', 'width', 'height']);
[...el.attributes].forEach(a => { if (!allowed.has(a.name)) el.removeAttribute(a.name); });
if (tag === 'img') el.setAttribute('crossorigin', 'anonymous');
// code blocks: keep plain code
if (tag === 'pre') {
const code = el.querySelector('code');
if (code) el.textContent = code.textContent || '';
}
});
// ensure correct base direction per block
clone.querySelectorAll('p,li,div,blockquote,h1,h2,h3,h4,h5,h6,td,th,dd,dt').forEach(el => {
if (!el.hasAttribute('dir')) el.setAttribute('dir', 'auto');
});
const leading = (clone.textContent || '').trim().slice(0, 140);
if (containsArabic(leading) && !clone.hasAttribute('dir')) clone.setAttribute('dir', 'rtl');
return clone;
}
// ---------- host + styles ----------
function buildRenderHost(cleanContent) {
// Keep host offscreen (NOT opacity:0) so ancestor opacity never affects rendering.
const host = document.createElement('div');
Object.assign(host.style, {
position: 'fixed',
left: '-10000px',
top: '0',
width: A4_W_PX + 'px',
zIndex: '-1',
pointerEvents: 'none'
});
const page = document.createElement('div');
Object.assign(page.style, {
width: A4_W_PX + 'px',
minHeight: A4_H_PX + 'px',
background: '#343541',
color: '#ececf1',
display: 'block',
margin: '0'
});
const inner = document.createElement('div');
inner.style.padding = PAD_PX + 'px';
inner.appendChild(cleanContent);
page.appendChild(inner);
const style = document.createElement('style');
style.textContent = `
@page { size: A4 portrait; margin: 0; }
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0 !important; padding: 0 !important; }
[dir="rtl"] { direction: rtl; unicode-bidi: isolate; }
[dir="ltr"] { direction: ltr; unicode-bidi: isolate; }
[dir="auto"] { unicode-bidi: isolate; }
:lang(ar), [dir="rtl"] {
font-family: "Noto Naskh Arabic","Noto Kufi Arabic",Tahoma,Arial,sans-serif !important;
font-variant-ligatures: contextual;
}
body, div, p, li, blockquote {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Noto Naskh Arabic","Noto Kufi Arabic", Arial, Tahoma, sans-serif;
line-height: 1.6;
color: #ececf1;
overflow-wrap: anywhere;
word-break: break-word;
}
hr { border: 0; border-top: 1px solid #565869; margin: 16px 0; }
h1,h2,h3,h4,h5,h6 { color: #fff; page-break-after: avoid !important; margin: 1em 0 .6em; }
p,ul,ol,pre,blockquote,table { margin: .85em 0; }
pre {
background:#000 !important;
color:#d1d5db !important;
border:1px solid #565869 !important;
border-radius:6px !important;
padding:12px !important;
font-family:"Cascadia Mono","SFMono-Regular",Consolas,"Liberation Mono",monospace !important;
font-size:11px !important;
line-height:1.4 !important;
white-space:pre-wrap !important;
overflow-wrap:anywhere !important;
direction:ltr !important;
unicode-bidi:isolate !important;
}
p code, li code, span code, code:not(pre code) {
font-family:"Cascadia Mono","SFMono-Regular",Consolas,"Liberation Mono",monospace !important;
background:#1e1e1e;
padding:2px 4px;
border-radius:4px;
font-size:.9em;
display:inline-block;
white-space:pre;
vertical-align:baseline;
direction:ltr;
unicode-bidi:isolate;
}
a {
color:#ececf1 !important;
text-decoration:none !important;
background:#40414f;
border:1px solid #565869;
border-radius:6px;
padding:3px 8px;
font-size:13px;
margin:0 2px;
display:inline;
white-space:nowrap;
vertical-align:baseline;
}
ul,ol { padding-inline-start:20px; }
li { margin-bottom:.5em; }
p:empty, div:empty, span:empty { display:none; }
`;
host.appendChild(style);
host.appendChild(page);
return { host, page };
}
// ---------- HTML2CANVAS + jsPDF path (automatic download; IMAGE-BASED = NO SELECTABLE TEXT) ----------
async function renderToPdfAuto(turnElement) {
const libs = await ensureLibsOrNull();
if (!libs) throw new Error('libs_missing');
const { html2canvas, JsPDF } = libs;
ensureArabicFonts();
await waitForFonts(document);
const cleanContent = extractAndCleanContent(turnElement);
const { host, page } = buildRenderHost(cleanContent);
document.body.appendChild(host);
const jsPDFCtor = (window.jspdf && window.jspdf.jsPDF) || JsPDF;
const pdf = new jsPDFCtor({ unit: 'mm', format: 'a4', orientation: 'portrait' });
const pageWidthMm = pdf.internal.pageSize.getWidth(); // 210
const pageHeightMm = pdf.internal.pageSize.getHeight(); // 297
const scale = Math.min(2, (window.devicePixelRatio || 1) * 1.5);
// Measure content height in CSS px
const contentHeightPx = Math.max(page.scrollHeight, A4_H_PX);
// Predict single-canvas height at chosen scale
const predictedFullCanvasH = Math.ceil(contentHeightPx * scale);
// If we'd exceed safe canvas limits, render page-by-page (prevents falling back to Print)
const shouldPageRender = predictedFullCanvasH > SAFE_MAX_CANVAS_DIM;
const baseOpts = {
scale,
useCORS: true,
allowTaint: false,
backgroundColor: '#343541',
foreignObjectRendering: true,
scrollX: 0,
scrollY: 0,
width: A4_W_PX,
windowWidth: A4_W_PX
};
const renderPaged = async () => {
const pages = Math.ceil(contentHeightPx / A4_H_PX);
// Pad the page element height to a clean multiple so each capture is a full A4 frame
page.style.height = (pages * A4_H_PX) + 'px';
for (let i = 0; i < pages; i++) {
const y = i * A4_H_PX;
const canvas = await html2canvas(page, {
...baseOpts,
x: 0,
y,
height: A4_H_PX,
windowHeight: A4_H_PX
});
const imgData = canvas.toDataURL('image/jpeg', 0.96);
if (i > 0) pdf.addPage();
pdf.addImage(imgData, 'JPEG', 0, 0, pageWidthMm, pageHeightMm, undefined, 'FAST');
}
};
const renderSingleThenSlice = async () => {
const canvas = await html2canvas(page, {
...baseOpts,
x: 0,
y: 0,
height: contentHeightPx,
windowHeight: contentHeightPx
});
// Slice the big canvas into A4 pages (no drift: x=0, width=full)
const pxPerMm = canvas.width / pageWidthMm;
const pageHeightPxScaled = Math.round(pageHeightMm * pxPerMm);
const sliceCanvas = document.createElement('canvas');
const ctx = sliceCanvas.getContext('2d');
sliceCanvas.width = canvas.width;
let offset = 0;
let pageIndex = 0;
while (offset < canvas.height) {
const sliceH = Math.min(pageHeightPxScaled, canvas.height - offset);
sliceCanvas.height = sliceH;
ctx.clearRect(0, 0, sliceCanvas.width, sliceCanvas.height);
ctx.drawImage(canvas, 0, -offset);
const imgData = sliceCanvas.toDataURL('image/jpeg', 0.96);
if (pageIndex > 0) pdf.addPage();
const sliceMm = sliceH / pxPerMm;
pdf.addImage(imgData, 'JPEG', 0, 0, pageWidthMm, sliceMm, undefined, 'FAST');
offset += sliceH;
pageIndex++;
}
};
try {
if (shouldPageRender) {
await renderPaged();
} else {
// Try the faster single-canvas path first; if it fails, fall back to paged (still no Print headers/footers).
try {
await renderSingleThenSlice();
} catch {
await renderPaged();
}
}
const chatTitle = (document.title || 'ChatGPT').trim();
const firstLine = (turnElement.textContent || '').trim().split('\n')[0].slice(0, 60);
const filename = sanitizeFilename(`${chatTitle} - ${firstLine}`) + '.pdf';
pdf.save(filename);
} finally {
host.remove();
}
}
// ---------- Beautiful instruction modal ----------
function showPrintInstructions() {
return new Promise((resolve) => {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.2s ease-out;
`;
const dialog = document.createElement('div');
dialog.style.cssText = `
background: #2f2f2f;
border-radius: 12px;
padding: 32px;
max-width: 520px;
width: 90%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
color: #ececf1;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
animation: slideUp 0.3s ease-out;
`;
dialog.innerHTML = `
<style>
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
</style>
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 20px;">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#10a37f" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="12" y1="18" x2="12" y2="12"></line>
<line x1="9" y1="15" x2="15" y2="15"></line>
</svg>
<h2 style="margin: 0; font-size: 22px; font-weight: 600; color: #fff;">PDF Download Mode</h2>
</div>
<div style="background: #1f1f1f; border-left: 3px solid #10a37f; padding: 16px; border-radius: 8px; margin-bottom: 20px;">
<div style="font-size: 14px; font-weight: 600; color: #10a37f; margin-bottom: 8px;">✓ Selectable Text Mode (Recommended)</div>
<div style="font-size: 14px; line-height: 1.6; color: #d1d5db;">
The print dialog is about to open. For best results with selectable text:
</div>
</div>
<div style="margin-bottom: 24px;">
<div style="display: flex; gap: 12px; margin-bottom: 16px;">
<div style="flex-shrink: 0; width: 28px; height: 28px; background: #10a37f; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 14px; color: #000;">1</div>
<div>
<div style="font-size: 15px; font-weight: 600; color: #fff; margin-bottom: 4px;">Choose the right destination</div>
<div style="font-size: 14px; line-height: 1.5; color: #d1d5db;">
Select <strong style="color: #10a37f;">"Save to PDF"</strong> (Chrome/Edge)<br>
<span style="color: #ff6b6b;">Avoid "Microsoft Print to PDF"</span> - it may not preserve text selection
</div>
</div>
</div>
<div style="display: flex; gap: 12px; margin-bottom: 16px;">
<div style="flex-shrink: 0; width: 28px; height: 28px; background: #10a37f; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 14px; color: #000;">2</div>
<div>
<div style="font-size: 15px; font-weight: 600; color: #fff; margin-bottom: 4px;">Disable headers and footers</div>
<div style="font-size: 14px; line-height: 1.5; color: #d1d5db;">
Uncheck "Headers and footers" option for a clean PDF
</div>
</div>
</div>
<div style="display: flex; gap: 12px;">
<div style="flex-shrink: 0; width: 28px; height: 28px; background: #565869; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 14px; color: #fff;">⚡</div>
<div>
<div style="font-size: 15px; font-weight: 600; color: #fff; margin-bottom: 4px;">Quick download mode</div>
<div style="font-size: 14px; line-height: 1.5; color: #d1d5db;">
Hold <kbd style="background: #000; padding: 2px 6px; border-radius: 4px; font-family: monospace; font-size: 13px; border: 1px solid #565869;">Shift</kbd> when clicking for instant download<br>
<span style="color: #999; font-size: 13px;">(image-based PDF, no text selection)</span>
</div>
</div>
</div>
</div>
<div style="display: flex; justify-content: flex-end; gap: 12px;">
<button id="pdfModalClose" style="
background: #10a37f;
color: #fff;
border: none;
padding: 10px 24px;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
" onmouseover="this.style.background='#0d8c6a'" onmouseout="this.style.background='#10a37f'">
Got it!
</button>
</div>
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
const closeModal = () => {
modal.style.animation = 'fadeIn 0.2s ease-out reverse';
setTimeout(() => {
modal.remove();
resolve();
}, 200);
};
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
dialog.querySelector('#pdfModalClose').addEventListener('click', closeModal);
// Auto-close after 10 seconds
setTimeout(closeModal, 10000);
});
}
// ---------- Print fallback (SELECTABLE TEXT - requires print dialog) ----------
async function printFallback(turnElement) {
ensureArabicFonts();
await waitForFonts(document);
const clean = extractAndCleanContent(turnElement);
const html = document.createElement('html');
const head = document.createElement('head');
const body = document.createElement('body');
const style = document.createElement('style');
style.textContent = `
@page { size: A4; margin: 0; }
@media print {
* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
html, body { margin:0; padding:0; }
body {
background:#343541;
color:#ececf1;
padding: ${PAD_MM}mm;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,
"Noto Naskh Arabic","Noto Kufi Arabic",Arial,Tahoma,sans-serif;
line-height:1.6;
overflow-wrap:anywhere;
word-break:break-word;
}
[dir="rtl"] { direction: rtl; unicode-bidi: isolate; }
[dir="ltr"] { direction: ltr; unicode-bidi: isolate; }
[dir="auto"] { unicode-bidi: isolate; }
:lang(ar), [dir="rtl"] {
font-family: "Noto Naskh Arabic","Noto Kufi Arabic",Tahoma,Arial,sans-serif !important;
font-variant-ligatures: contextual;
}
hr { border: 0; border-top: 1px solid #565869; margin: 16px 0; }
h1,h2,h3,h4,h5,h6 { color:#fff; margin: 1em 0 .6em; }
p,ul,ol,pre,blockquote,table { margin: .85em 0; }
pre {
background:#000!important;color:#d1d5db!important;border:1px solid #565869!important;border-radius:6px!important;
padding:12px!important;font-family:"Cascadia Mono","SFMono-Regular",Consolas,"Liberation Mono",monospace!important;
font-size:11px!important; line-height:1.4!important; white-space:pre-wrap!important; overflow-wrap:anywhere!important;
direction:ltr!important; unicode-bidi:isolate!important;
}
p code, li code, span code, code:not(pre code) {
font-family:"Cascadia Mono","SFMono-Regular",Consolas,"Liberation Mono",monospace!important;
background:#1e1e1e; padding:2px 4px; border-radius:4px; font-size:.9em; display:inline-block; white-space:pre;
vertical-align:baseline; direction:ltr; unicode-bidi:isolate;
}
a {
color:#ececf1!important; text-decoration:none!important; background:#40414f; border:1px solid #565869; border-radius:6px;
padding:3px 8px; font-size:13px; margin:0 2px; display:inline; white-space:nowrap; vertical-align:baseline;
}
ul,ol { padding-inline-start:20px; } li { margin-bottom:.5em; } p:empty, div:empty, span:empty { display:none; }
`;
head.appendChild(style);
body.appendChild(clean);
html.appendChild(head);
html.appendChild(body);
const title = (document.title || 'ChatGPT').trim();
const firstLine = (turnElement.textContent || '').trim().split('\n')[0].slice(0, 60);
const iframe = document.createElement('iframe');
Object.assign(iframe.style, { position:'fixed', left:'-10000px', top:'0', width:'0', height:'0', opacity: '0' });
document.body.appendChild(iframe);
const doc = iframe.contentDocument;
doc.open();
doc.write('<!doctype html>' + html.outerHTML);
doc.title = sanitizeFilename(`${title} - ${firstLine}`);
doc.close();
const wait = (ms)=>new Promise(r=>setTimeout(r,ms));
try {
if (doc.fonts) await doc.fonts.ready;
const imgs = Array.from(doc.images || []);
await Promise.all(imgs.map(img => img.complete ? Promise.resolve() : new Promise(res => { img.onload = img.onerror = res; })));
await wait(100);
} catch {}
// Show instructions modal FIRST, then open print dialog
if (!window.__cgptpdf_print_hint_shown) {
window.__cgptpdf_print_hint_shown = true;
await showPrintInstructions();
}
iframe.contentWindow.focus();
iframe.contentWindow.print();
setTimeout(() => iframe.remove(), 10000);
}
// ---------- UI wiring ----------
const pdfIconSvg = `
<svg width="18" height="18" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
<path d="M11.5 2.5H5.5C4.95 2.5 4.5 2.95 4.5 3.5V16.5C4.5 17.05 4.95 17.5 5.5 17.5H14.5C15.05 17.5 15.5 17.05 15.5 16.5V6.5L11.5 2.5Z" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M11.5 2.5V6.5H15.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M7.5 12.5L10 15L12.5 12.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M10 15V9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
`;
function addDownloadButton(turnElement) {
const copyButton = turnElement.querySelector('button[data-testid*="copy"]');
if (!copyButton) return;
const container = copyButton.parentElement;
if (!container || container.querySelector('.download-pdf-button')) return;
const btn = document.createElement('button');
btn.className = 'text-token-text-secondary hover:bg-token-bg-secondary rounded-lg download-pdf-button';
btn.title = 'Download as PDF (click = selectable text, shift+click = image PDF)';
btn.innerHTML = `<span class="touch:w-10 flex h-8 w-8 items-center justify-center">${pdfIconSvg}</span>`;
// UPDATED CLICK HANDLER: Normal click = selectable text; Shift+click = image PDF
btn.addEventListener('click', async (e) => {
e.stopPropagation();
btn.disabled = true;
const prev = btn.innerHTML;
try {
btn.innerHTML = '<span class="touch:w-10 flex h-8 w-8 items-center justify-center">…</span>';
// NORMAL CLICK => selectable text via print dialog (best RTL/Arabic support)
// SHIFT-CLICK => one-click image PDF (current behavior; NOT selectable)
if (!e.shiftKey) {
await printFallback(turnElement);
return;
}
// Shift-click path: your existing auto-download image PDF
await renderToPdfAuto(turnElement);
} catch (err) {
// If anything fails, still fall back to print
await printFallback(turnElement);
} finally {
btn.disabled = false;
btn.innerHTML = prev;
}
});
copyButton.insertAdjacentElement('afterend', btn);
}
function processAllMessages() {
document.querySelectorAll('article[data-testid^="conversation-turn-"]').forEach(addDownloadButton);
}
let debounceTimer;
const observer = new MutationObserver(() => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(processAllMessages, 200);
});
function initialize() {
ensureArabicFonts();
processAllMessages();
const main = document.querySelector('main');
if (main) observer.observe(main, { childList: true, subtree: true });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
})();
@minanagehsalalma
Copy link
Author

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment