AI Chat to Docx, Md, Html, Pdf, Json, Txt

Export and Copy AI answers with multiple formats. Applied for ChatGPT, Gemini, Aistudio, Notebooklm, Grok, Claude, Mistral, Deepseek, Kimi, Perplexity, Liner, Scienceos, Evidencehunt, Spacefrontiers. Added tab title for Gemini, Evidencehunt.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         AI Chat to Docx, Md, Html, Pdf, Json, Txt
// @namespace    https://greasyfork.org/
// @version      3.4
// @description  Export and Copy AI answers with multiple formats. Applied for ChatGPT, Gemini, Aistudio, Notebooklm, Grok, Claude, Mistral, Deepseek, Kimi, Perplexity, Liner, Scienceos, Evidencehunt, Spacefrontiers. Added tab title for Gemini, Evidencehunt.
// @author       Bui Quoc Dung
// @match        https://chatgpt.com/*
// @match        https://gemini.google.com/*
// @match        https://aistudio.google.com/*
// @match        https://notebooklm.google.com/*
// @match        https://grok.com/*
// @match        https://claude.ai/*
// @match        https://chat.mistral.ai/*
// @match        https://www.perplexity.ai/*
// @match        https://chat.deepseek.com/*
// @match        https://www.kimi.com/*
// @match        https://app.liner.com/*
// @match        https://app.scienceos.ai/*
// @match        https://evidencehunt.com/*
// @match        https://spacefrontiers.org/*
// @grant        GM_addStyle
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/html-docx.min.js
// @require      https://unpkg.com/turndown/dist/turndown.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/he.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js
// ==/UserScript==

(function () {
    'use strict';

    // ─── Styles ───────────────────────────────────────────────────────────────
    GM_addStyle(`
        .ai-exporter-toolbar {
            margin-top: 10px;
            margin-bottom: 10px;
            display: flex;
            gap: 4px;
            flex-wrap: wrap;
            clear: both;
            justify-content: flex-end;
            width: 100%;
            align-items: center;
        }
        .ai-exporter-btn {
            padding: 2px 10px;
            font-size: 13px;
            line-height: 20px;
            border-radius: 12px;
            border: 1px solid #dadce0;
            background: transparent;
            cursor: pointer;
            font-family: Google Sans, Roboto, Arial, sans-serif;
            color: CanvasText;
            white-space: nowrap;
            transition: background 0.1s;
        }
        .ai-exporter-btn:hover {
            background: rgba(128,128,128,0.08);
        }
        .ai-exporter-dropdown {
            position: relative;
            display: inline-block;
        }
        .ai-exporter-dropdown-menu {
            display: none;
            position: absolute;
            bottom: calc(100% + 4px);
            right: 0;
            background: Canvas;
            border: 1px solid #dadce0;
            border-radius: 10px;
            box-shadow: 0 4px 16px rgba(0,0,0,0.13);
            z-index: 99999;
            min-width: 130px;
            padding: 4px;
        }
        .ai-exporter-dropdown.open .ai-exporter-dropdown-menu {
            display: block;
        }
        .ai-exporter-dropdown.open .ai-exporter-btn {
            background: rgba(128,128,128,0.08);
        }
        .ai-exporter-dropdown-item {
            display: flex;
            align-items: center;
            gap: 8px;
            width: 100%;
            padding: 6px 12px;
            font-size: 13px;
            font-family: Google Sans, Roboto, Arial, sans-serif;
            color: CanvasText;
            background: none;
            border: none;
            border-radius: 7px;
            cursor: pointer;
            white-space: nowrap;
            box-sizing: border-box;
            transition: background 0.1s;
        }
        .ai-exporter-dropdown-item:hover {
            background: rgba(128,128,128,0.1);
        }
        .ai-exporter-separator {
            width: 1px;
            height: 18px;
            background: #dadce0;
            margin: 0 4px;
            flex-shrink: 0;
        }
    `);

    // ─── Site configs ─────────────────────────────────────────────────────────
    const SITE_CONFIGS = {
        chatgpt: {
            domain: 'chatgpt.com',
            user: 'div[data-message-author-role="user"]',
            ai_response: 'div[data-message-author-role="assistant"]',
            attach_to: '.markdown',
            siteName: 'ChatGPT',
            nameSelector: 'a[data-active] .truncate'
        },
        gemini: {
            domain: 'gemini.google.com',
            user: '.query-text',
            ai_response: '.model-response-text',
            attach_to: null,
            siteName: 'Gemini',
            nameSelector: '.gds-title-m.ng-star-inserted',
            updateTabTitle: true
        },
        aistudio: {
            domain: 'aistudio.google.com',
            user: '.user-prompt-container .text-chunk.ng-star-inserted',
            ai_response: '.model-prompt-container .text-chunk.ng-star-inserted',
            attach_to: null,
            siteName: 'AIStudio',
            nameSelector: 'h1.actions.mode-title, h1.actions.v3-font-headline-2'
        },
        notebooklm: {
            domain: 'notebooklm.google.com',
            user: 'chat-message .from-user-container',
            ai_response: 'chat-message .to-user-container',
            attach_to: ':last-child',
            siteName: 'NotebookLM',
            nameSelector: '.title-container.ng-star-inserted'
        },
        grok: {
            domain: 'grok.com',
            user: '.relative.group.flex.flex-col.justify-center.items-end',
            ai_response: '.relative.group.flex.flex-col.justify-center.items-start',
            attach_to: null,
            siteName: 'Grok',
            nameSelector: 'a.border-border-l1 span'
        },
        claude: {
            domain: 'claude.ai',
            user: 'div.group.relative.inline-flex',
            ai_response: '.group.relative.pb-3',
            attach_to: null,
            siteName: 'Claude',
            nameSelector: '.truncate.font-base-bold'
        },
        mistral: {
            domain: 'chat.mistral.ai',
            user: 'div[data-message-author-role="user"] div[dir="auto"]',
            ai_response: 'div[data-message-author-role="assistant"] div[data-message-part-type="answer"]',
            attach_to: null,
            siteName: 'Mistral',
            nameSelector: 'a[data-active="true"] .block'
        },
        deepseek: {
            domain: 'chat.deepseek.com',
            user: '._9663006 .fbb737a4',
            ai_response: '._43c05b5',
            attach_to: null,
            siteName: 'Deepseek',
            nameSelector: '.afa34042.e37a04e4.e0a1edb7'
        },
        kimi: {
            domain: 'www.kimi.com',
            user: '.user-content',
            ai_response: '.markdown-container',
            attach_to: null,
            siteName: 'Kimi',
            nameSelector: '.chat-header-content'
        },
        perplexity: {
            domain: 'www.perplexity.ai',
            user: 'div.group\\/title',
            ai_response: '.leading-relaxed.break-words.min-w-0',
            attach_to: null,
            siteName: 'Perplexity',
            nameSelector: 'title'
        },
        liner: {
            domain: 'app.liner.com',
            user: '.gap-positive-300.flex.flex-col',
            ai_response: '.search-result-answer.w-full.break-words',
            attach_to: null,
            siteName: 'Liner',
            nameSelector: ''
        },
        scienceos: {
            domain: 'app.scienceos.ai',
            user: 'div[data-prompt]',
            ai_response: '.tailwind',
            attach_to: null,
            siteName: 'ScienceOS',
            nameSelector: 'header'
        },
        evidencehunt: {
            domain: 'evidencehunt.com',
            user: '.chat__message:has(.message__user-image) .message__content p',
            ai_response: '.chat__message:has(.message__eh-image) .message__content',
            attach_to: null,
            siteName: 'EvidenceHunt',
            nameSelector: 'button.bg-primary-lighten-1 .chip-button__text',
            updateTabTitle: true
        },
        spacefrontiers: {
            domain: 'spacefrontiers.org',
            user: '.inline.whitespace-pre-line',
            ai_response: '.citation-processed-content',
            attach_to: null,
            siteName: 'SpaceFrontiers',
            nameSelector: 'h1.whitespace-pre-line'
        },
    };

    const CONFIG = Object.values(SITE_CONFIGS).find(c => window.location.hostname.includes(c.domain));
    if (!CONFIG) return;

    // ─── Utilities ────────────────────────────────────────────────────────────
    const INJECTED_CLASS = 'ai-exporter-toolbar';

    const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
    turndownService.keep(['table', 'tr', 'td', 'th', 'tbody', 'thead']);

    function debounce(fn, delay) {
        let timer;
        return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); };
    }

    function getTimestamp() {
        return new Date().toISOString().slice(0, 19).replace(/:/g, '-');
    }

    function getConversationName() {
        const el = CONFIG.nameSelector && document.querySelector(CONFIG.nameSelector);
        if (!el) return '';
        return el.textContent.trim().replace(/[<>:"/\\|?*]/g, '-').substring(0, 50);
    }

    function generateFileName(index = null) {
        const parts = [CONFIG.siteName, getConversationName(), index !== null ? `Response-${index}` : 'Full-Chat', getTimestamp()];
        return parts.filter(Boolean).join('-');
    }

    function cleanNode(element) {
        const clone = element.cloneNode(true);
        clone.querySelectorAll(`.${INJECTED_CLASS}, button, .copy-button, [aria-label*="Copy"], .not-export`).forEach(el => el.remove());
        return clone;
    }

    function download(blob, filename) {
        const url = URL.createObjectURL(blob);
        const a = Object.assign(document.createElement('a'), { href: url, download: filename });
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    function updateTabTitle() {
        if (!CONFIG.updateTabTitle || !CONFIG.nameSelector) return;
        const el = document.querySelector(CONFIG.nameSelector);
        if (el?.textContent.trim()) document.title = `${el.textContent.trim()} - ${CONFIG.siteName}`;
    }

    // ─── DOM helpers ──────────────────────────────────────────────────────────
    function createButton(text, onClick) {
        const btn = document.createElement('button');
        btn.className = 'ai-exporter-btn';
        btn.textContent = text;
        btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); onClick(e); };
        return btn;
    }

    function createSeparator() {
        const sep = document.createElement('div');
        sep.className = 'ai-exporter-separator';
        return sep;
    }

    function createDropdown(label, items) {
        const wrapper = document.createElement('div');
        wrapper.className = 'ai-exporter-dropdown';

        const toggle = document.createElement('button');
        toggle.className = 'ai-exporter-btn';
        toggle.textContent = label;
        toggle.onclick = (e) => {
            e.preventDefault();
            e.stopPropagation();
            document.querySelectorAll('.ai-exporter-dropdown.open').forEach(d => { if (d !== wrapper) d.classList.remove('open'); });
            wrapper.classList.toggle('open');
        };

        const menu = document.createElement('div');
        menu.className = 'ai-exporter-dropdown-menu';

        for (const item of items) {
            const btn = document.createElement('button');
            btn.className = 'ai-exporter-dropdown-item';
            btn.textContent = item.label;
            btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); wrapper.classList.remove('open'); item.onClick(e); };
            menu.appendChild(btn);
        }

        wrapper.appendChild(toggle);
        wrapper.appendChild(menu);
        return wrapper;
    }

    document.addEventListener('click', (e) => {
        if (!e.target.closest('.ai-exporter-dropdown')) {
            document.querySelectorAll('.ai-exporter-dropdown.open').forEach(d => d.classList.remove('open'));
        }
    });

    // ─── Export functions ─────────────────────────────────────────────────────
    function exportWord(element, filename) {
        const fullHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
            body{font-family:sans-serif;line-height:1.5}
            table{border-collapse:collapse;width:100%}
            th,td{border:1px solid #000;padding:5px}
            pre{background:#f4f4f4;padding:10px;border-radius:5px}
            h1{font-size:20px;font-weight:bold;color:#2d3748;margin-top:20px}
        </style></head><body>${cleanNode(element).innerHTML}</body></html>`;
        try { download(window.htmlDocx.asBlob(fullHtml), filename + '.docx'); } catch (e) { console.error(e); }
    }

    function exportMarkdown(element, filename) {
        try {
            const md = turndownService.turndown(cleanNode(element));
            download(new Blob([md], { type: 'text/markdown;charset=utf-8' }), filename + '.md');
        } catch (e) { console.error(e); }
    }

    function exportHTML(element, filename) {
        const fullHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
            body{font-family:sans-serif;line-height:1.5;padding:20px;max-width:900px;margin:auto}
            table{border-collapse:collapse;width:100%;margin:10px 0}
            th,td{border:1px solid #ddd;padding:8px;text-align:left}
            pre{background:#f4f4f4;padding:10px;border-radius:5px;overflow-x:auto}
        </style></head><body>${cleanNode(element).innerHTML}</body></html>`;
        download(new Blob([fullHtml], { type: 'text/html;charset=utf-8' }), filename + '.html');
    }

    function exportJSON(nodes, filename) {
        const list = Array.isArray(nodes) ? nodes : [nodes];
        const data = list.map(n => ({
            role: n.matches(CONFIG.user) ? 'user' : 'assistant',
            content: (window.he ? window.he.decode : x => x)(cleanNode(n).innerText.trim())
        }));
        download(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }), filename + '.json');
    }

    function exportText(element, filename) {
        const text = (window.he ? window.he.decode : x => x)(cleanNode(element).innerText.trim());
        download(new Blob([text], { type: 'text/plain;charset=utf-8' }), filename + '.txt');
    }

    async function exportPDFGemini(element, filename, btn) {
        const original = btn?.textContent ?? '';
        if (btn) { btn.textContent = 'Wait...'; btn.disabled = true; }
        try {
            const cleaned = cleanNode(element);
            const { jsPDF } = window.jspdf;
            const pdf = new jsPDF('p', 'mm', 'a4');
            const pageWidth = pdf.internal.pageSize.getWidth();
            const pageHeight = pdf.internal.pageSize.getHeight();
            const margin = 15;
            const maxWidth = pageWidth - margin * 2;
            let y = margin;
            const LH = 7, CLH = 5;

            function addText(text, { fontSize = 11, fontStyle = 'normal', fontFamily = 'helvetica', lineHeight = LH, indent = 0 } = {}) {
                if (!text?.trim()) return;
                pdf.setFontSize(fontSize);
                pdf.setFont(fontFamily, fontStyle);
                for (const line of pdf.splitTextToSize(text, maxWidth - indent)) {
                    if (y + lineHeight > pageHeight - margin) { pdf.addPage(); y = margin; }
                    pdf.text(line, margin + indent, y);
                    y += lineHeight;
                }
            }

            function parseNode(node) {
                if (!node) return;
                const tag = node.tagName?.toLowerCase();
                const text = node.innerText?.trim();
                if (!text) return;
                if (tag === 'h1') { y += 5; addText(text, { fontSize: 16, fontStyle: 'bold' }); y += 3; return; }
                if (tag === 'h2') { y += 4; addText(text, { fontSize: 14, fontStyle: 'bold' }); y += 2; return; }
                if (tag === 'h3') { y += 3; addText(text, { fontSize: 12, fontStyle: 'bold' }); y += 2; return; }
                if (tag === 'pre' || tag === 'code' || node.className?.includes('code')) {
                    y += 3;
                    const lines = pdf.splitTextToSize(text, maxWidth - 10);
                    const bh = lines.length * CLH + 4;
                    if (y + bh > pageHeight - margin) { pdf.addPage(); y = margin; }
                    pdf.setFillColor(245, 245, 245);
                    pdf.rect(margin, y - 2, maxWidth, bh, 'F');
                    addText(text, { fontFamily: 'courier', fontSize: 9, lineHeight: CLH, indent: 5 });
                    y += 3; return;
                }
                if (tag === 'li') { addText('• ' + text, { indent: 5 }); return; }
                if (tag === 'blockquote') {
                    y += 2;
                    pdf.setDrawColor(200, 200, 200); pdf.setLineWidth(0.5);
                    const sy = y;
                    addText(text, { indent: 10, fontStyle: 'italic' });
                    pdf.line(margin + 5, sy, margin + 5, y);
                    y += 2; return;
                }
                if (tag === 'strong' || tag === 'b') { addText(text, { fontStyle: 'bold' }); return; }
                if (tag === 'p') { addText(text); y += 2; return; }
                if (tag === 'table') {
                    y += 3;
                    node.querySelectorAll('tr').forEach(row => {
                        addText(Array.from(row.querySelectorAll('td,th')).map(c => c.innerText.trim()).join(' | '), { fontSize: 9 });
                    });
                    y += 3; return;
                }
                if (node.children?.length) Array.from(node.children).forEach(parseNode);
                else addText(text);
            }

            if (cleaned.children?.length) Array.from(cleaned.children).forEach(parseNode);
            else addText(cleaned.innerText);

            pdf.save(filename + '.pdf');
            if (btn) { btn.textContent = 'Done'; btn.disabled = false; setTimeout(() => btn.textContent = original, 2000); }
        } catch (e) {
            console.error(e);
            if (btn) { btn.textContent = 'Error!'; btn.disabled = false; setTimeout(() => btn.textContent = original, 2000); }
        }
    }

    async function exportPDF(element, filename, btn) {
        if (window.location.hostname.includes('gemini.google.com')) return exportPDFGemini(element, filename, btn);
        const original = btn?.textContent ?? '';
        if (btn) { btn.textContent = 'Wait...'; btn.disabled = true; }
        try {
            await new Promise(r => setTimeout(r, 50));
            const cleaned = cleanNode(element);
            const container = Object.assign(document.createElement('div'), {
                style: 'font-family:Arial,sans-serif;font-size:14px;line-height:1.6;padding:20px;max-width:800px;color:#000;background:#fff;position:absolute;left:-9999px'
            });
            container.appendChild(cleaned);
            document.body.appendChild(container);

            const canvas = await window.html2canvas(container, { scale: 2, useCORS: true, logging: false, windowWidth: 800, backgroundColor: '#ffffff' });
            document.body.removeChild(container);

            const { jsPDF } = window.jspdf;
            const pdf = new jsPDF('p', 'mm', 'a4');
            const pw = pdf.internal.pageSize.getWidth(), ph = pdf.internal.pageSize.getHeight();
            const iw = pw - 20, ih = (canvas.height * iw) / canvas.width;
            const imgData = canvas.toDataURL('image/jpeg', 0.85);

            let left = ih, pos = 10;
            pdf.addImage(imgData, 'JPEG', 10, pos, iw, ih);
            left -= (ph - 20);
            while (left > 0) { pos = left - ih + 10; pdf.addPage(); pdf.addImage(imgData, 'JPEG', 10, pos, iw, ih); left -= (ph - 20); }

            pdf.save(filename + '.pdf');
            if (btn) { btn.textContent = 'Done!'; btn.disabled = false; setTimeout(() => btn.textContent = original, 2000); }
        } catch (e) {
            console.error(e);
            if (btn) { btn.textContent = 'Error!'; btn.disabled = false; setTimeout(() => btn.textContent = original, 2000); }
        }
    }

    async function copyContent(element, btn) {
        try {
            const cleaned = cleanNode(element);
            await navigator.clipboard.write([new ClipboardItem({
                'text/html': new Blob([cleaned.innerHTML], { type: 'text/html' }),
                'text/plain': new Blob([cleaned.innerText], { type: 'text/plain' })
            })]);
            const orig = btn.textContent; btn.textContent = 'Copied'; setTimeout(() => btn.textContent = orig, 2000);
        } catch (e) { console.error(e); }
    }

    async function copyMarkdown(element, btn) {
        try {
            await navigator.clipboard.writeText(turndownService.turndown(cleanNode(element)));
            const orig = btn.textContent; btn.textContent = 'Copied'; setTimeout(() => btn.textContent = orig, 2000);
        } catch (e) { console.error(e); }
    }

    // ─── Combined helpers ─────────────────────────────────────────────────────
    function getCombinedNodes() {
        return Array.from(document.querySelectorAll([CONFIG.ai_response, CONFIG.user].join(',')))
            .sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
    }

    function getCombinedHTML() {
        const container = document.createElement('div');
        for (const node of getCombinedNodes()) {
            const wrapper = document.createElement('div');
            wrapper.style.marginBottom = '20px';
            if (node.matches(CONFIG.user)) {
                const h1 = Object.assign(document.createElement('h1'), { textContent: node.innerText.trim() });
                h1.style.cssText = 'font-size:16pt;font-family:sans-serif;font-weight:bold;margin-bottom:10px;color:#000';
                wrapper.appendChild(h1);
            } else {
                wrapper.appendChild(cleanNode(node));
            }
            container.appendChild(wrapper);
        }
        return container;
    }

    // ─── Inject ───────────────────────────────────────────────────────────────
    const EXPORT_ITEMS = (element, name) => [
        { label: 'Word (.docx)',     onClick: ()  => exportWord(element, name) },
        { label: 'Markdown (.md)',   onClick: ()  => exportMarkdown(element, name) },
        { label: 'HTML (.html)',     onClick: ()  => exportHTML(element, name) },
        { label: 'PDF (.pdf)',       onClick: (e) => exportPDF(element, name, e.target) },
        { label: 'JSON (.json)',     onClick: ()  => exportJSON(element, name) },
        { label: 'Text (.txt)',      onClick: ()  => exportText(element, name) },
    ];

    function inject() {
        const answers = document.querySelectorAll(CONFIG.ai_response);
        answers.forEach((answer, index) => {
            if (answer.querySelector(`.${INJECTED_CLASS}`)) return;

            let target = answer;
            if (CONFIG.attach_to === ':last-child') { if (answer.lastElementChild) target = answer.lastElementChild; }
            else if (CONFIG.attach_to) { const inner = answer.querySelector(CONFIG.attach_to); if (inner) target = inner; }

            const name = generateFileName(index + 1);

            const toolbar = document.createElement('div');
            toolbar.className = INJECTED_CLASS;

            if (index === answers.length - 1) {
                const nameAll = generateFileName(null);
                const allGroup = document.createElement('div');
                allGroup.style.cssText = 'display:flex;gap:4px;align-items:center;margin-right:auto;';
                allGroup.appendChild(createDropdown('Export all', EXPORT_ITEMS(getCombinedHTML(), nameAll).map((item, i) =>
                    i === 4 ? { ...item, onClick: () => exportJSON(getCombinedNodes(), nameAll) } : item
                )));
                allGroup.appendChild(createButton('Copy all (md)', (e) => copyMarkdown(getCombinedHTML(), e.target)));
                toolbar.appendChild(allGroup);
            }

            toolbar.appendChild(createDropdown('Export', EXPORT_ITEMS(answer, name)));
            toolbar.appendChild(createButton('Copy (docx)', (e) => copyContent(answer, e.target)));
            toolbar.appendChild(createButton('Copy (md)',   (e) => copyMarkdown(answer, e.target)));

            target.appendChild(toolbar);
        });

        updateTabTitle();
    }

    const observer = new MutationObserver(debounce(inject, 1500));
    observer.observe(document.body, { childList: true, subtree: true });
    setTimeout(() => { inject(); updateTabTitle(); }, 2000);

})();