Hi everyone!! ![]()
Here’s how you can add a Share to PDF button to your Pickaxe. This exports the visible chat thread to a PDF using custom javascript. Inspired by our dynamic-loader so it won’t clash with your page!!
To do this, you’ll:
-
Use a script embed (not an iframe)
-
Add one more
<script>that loadshtml2pdf.js, mounts a floating button, and captures the chat DOM to PDF -
Everything is namespaced and guarded to avoid duplicate injections
Example
Your Pickaxe script embed →
<script src="https://studio.pickaxe.co/api/embed/bundle.js" defer></script>
Script begins here! →
<script>
(() => {
// prevent dupe loads if snippet appears more than once
if (window.__PICKAXE_PDF_LOADER__) return;
window.__PICKAXE_PDF_LOADER__ = true;
// ---- config (edit these!!) ----
const NS = 'pa-pdf';
const CONFIG = {
buttonText: 'Share',
fileName: 'chat.pdf',
// if you customized your chat DOM, update this selector
chatSelector: '[data-pickaxe-embed] section, [data-pickaxe-embed] .chat, [data-pickaxe-embed]',
position: { bottom: '24px', right: '24px' }
};
// load a script exactly once by URL
const loadOnce = (src) => new Promise((res, rej) => {
const id = `${NS}-script-${btoa(src)}`;
if (document.getElementById(id)) return res();
const s = document.createElement('script');
s.id = id; s.src = src; s.defer = true;
s.onload = res; s.onerror = rej;
document.head.appendChild(s);
});
// wait for the Pickaxe embed to exist in the DOM
const waitForEmbed = () => new Promise((resolve) => {
if (document.querySelector('[data-pickaxe-embed]')) return resolve();
const obs = new MutationObserver(() => {
if (document.querySelector('[data-pickaxe-embed]')) { obs.disconnect(); resolve(); }
});
obs.observe(document.documentElement || document.body, { childList: true, subtree: true });
});
// find the chat container
const getChatRoot = () => {
const root = document.querySelector('[data-pickaxe-embed]');
if (root) {
const inner = root.querySelector('section, .chat, .messages, [role="log"]');
return inner || root;
}
return document.querySelector(CONFIG.chatSelector) || document.body;
};
// build a copy w/ stripped inputs/toolbars
const buildPrintable = (chatEl) => {
const clone = chatEl.cloneNode(true);
clone.querySelectorAll('textarea, input, button, [contenteditable], .composer, .toolbar, [data-action]')
.forEach(n => n.remove());
// optional: hide reasoning / system blocks (uncomment if you want them omitted!!)
// clone.querySelectorAll('.reasoning, think, [data-reasoning]').forEach(n => n.remove());
const wrap = document.createElement('div');
wrap.style.padding = '16px';
wrap.style.background = 'white';
wrap.style.color = '#111';
wrap.style.maxWidth = '920px';
wrap.style.margin = '0 auto';
wrap.appendChild(clone);
return wrap;
};
// create the floating Share button
const mountButton = () => {
if (document.querySelector(`.${NS}-btn`)) return null;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = `${NS}-btn`;
btn.textContent = CONFIG.buttonText;
Object.assign(btn.style, {
position: 'fixed',
zIndex: '2147483646',
bottom: CONFIG.position.bottom,
right: CONFIG.position.right,
padding: '10px 14px',
borderRadius: '10px',
border: '1px solid rgba(0,0,0,.12)',
background: 'white',
boxShadow: '0 8px 24px rgba(0,0,0,.15)',
font: '500 14px/1.2 system-ui, -apple-system, Segoe UI, Roboto, sans-serif',
cursor: 'pointer'
});
btn.setAttribute('aria-label', 'Share chat as PDF');
document.body.appendChild(btn);
return btn;
};
// wire everything up
const init = async () => {
await waitForEmbed();
await loadOnce('https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js');
const btn = mountButton();
if (!btn) return;
btn.addEventListener('click', () => {
try {
const chatEl = getChatRoot();
const node = buildPrintable(chatEl);
window.html2pdf().set({
margin: [10,10,10,10],
filename: CONFIG.fileName,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
}).from(node).save();
} catch (e) {
console.error('[Pickaxe PDF] export failed:', e);
alert('Sorry, there was a problem exporting your PDF.');
}
}, { passive: true });
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();
</script>
CUSTOMIZATION
You can tweak the filename, button position, or which elements are included/excluded in the PDF (see the comments in the code above)
Notes
-
Use script embed for this (iframes isolate content and can’t be captured from the parent page).
-
The snippet is namespaced (
pa-pdf) and guarded withwindow.__PICKAXE_PDF_LOADER__so you won’t get duplicates if it’s included in both a layout and a page -
If your chat wrapper is customized, update
chatSelectorto match your DOM
If any of this is super duper confusing but you still want to try: we’ll reply below with a short glossary defining some of the logic talked about above
Happy Building and special shoutout to one of our users @leoreche for this inspiration on this one! ![]()