AI Chat to Microsoft Word, Markdown, Html, Pdf, Json, Txt

Export AI answers with multiple formats. Applied for ChatGPT, Gemini, Aistudio, Notebooklm, Grok, Claude, Mistral, Perplexity, Deepseek, Scienceos, Evidencehunt, Spacefrontiers.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         AI Chat to Microsoft Word, Markdown, Html, Pdf, Json, Txt
// @namespace    https://greasyfork.org/
// @version      2.7
// @description  Export AI answers with multiple formats. Applied for ChatGPT, Gemini, Aistudio, Notebooklm, Grok, Claude, Mistral, Perplexity, Deepseek, Scienceos, Evidencehunt, Spacefrontiers.
// @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://app.scienceos.ai/*
// @match        https://evidencehunt.com/*
// @match        https://spacefrontiers.org/*
// @grant        none
// @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';

    const COMMON_CONTAINER_STYLE = {
        marginTop: '10px',
        marginBottom: '10px',
        display: 'flex',
        gap: '4px',
        flexWrap: 'wrap',
        clear: 'both',
        justifyContent: 'flex-end',
        width: '100%'
    };

    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: '.conversation-title.gds-title-m'
        },
        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'
        },
        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'
        },
        deepseek: {
            domain: 'chat.deepseek.com',
            user: '._9663006 .fbb737a4',
            ai_response: '._43c05b5',
            attach_to: null,
            siteName: 'Deepseek',
            nameSelector: '.afa34042.e37a04e4.e0a1edb7'
        },
        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'
        },
        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 = (function() {
        const hostname = window.location.hostname;
        for (const key in SITE_CONFIGS) {
            if (hostname.includes(SITE_CONFIGS[key].domain)) return SITE_CONFIGS[key];
        }
        return null;
    })();

    if (!CONFIG) return;

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

    function getConversationName() {
        if (!CONFIG.nameSelector) return '';

        const nameElement = document.querySelector(CONFIG.nameSelector);
        if (nameElement) {
            let name = nameElement.textContent.trim();
            name = name.replace(/[<>:"/\\|?*]/g, '-');
            if (name.length > 50) {
                name = name.substring(0, 50);
            }
            return name;
        }
        return '';
    }

    function generateFileName(baseName, index = null) {
        const timestamp = getTimestamp();
        const siteName = CONFIG.siteName;
        const conversationName = getConversationName();

        let fileName = siteName;
        if (conversationName) {
            fileName += `-${conversationName}`;
        }
        if (index !== null) {
            fileName += `-Response-${index}`;
        } else {
            fileName += `-Full-Chat`;
        }
        fileName += `-${timestamp}`;
        return fileName;
    }

    function createButton(text, onClick) {
        const btn = document.createElement('button');
        btn.textContent = text;
        Object.assign(btn.style, {
            marginLeft: '8px',
            padding: '2px 10px',
            fontSize: '13px',
            lineHeight: '20px',
            borderRadius: '12px',
            border: '1px solid #dadce0',
            backgroundColor: 'transparent',
            cursor: 'pointer',
            fontFamily: 'Google Sans, Roboto, Arial, sans-serif',
            transition: 'all 0.1s',
            color: 'CanvasText'
        });

        btn.onclick = (e) => {
            e.preventDefault();
            e.stopPropagation();
            onClick(e);
        };
        return btn;
    }

    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 = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

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

    const Exporters = {
        html: (el, name) => {
            const cleaned = cleanNode(el);
            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>${cleaned.innerHTML}</body></html>`;
            download(new Blob([fullHtml], { type: 'text/html;charset=utf-8' }), name + '.html');
        },
        json: (nodes, name) => {
            const isArray = Array.isArray(nodes);
            const nodeList = isArray ? nodes : [nodes];
            const data = nodeList.map(n => ({
                role: n.matches(CONFIG.user) ? 'user' : 'assistant',
                content: window.he ? window.he.decode(cleanNode(n).innerText.trim()) : cleanNode(n).innerText.trim()
            }));
            download(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }), name + '.json');
        },
        text: (el, name) => {
            const text = window.he ? window.he.decode(cleanNode(el).innerText.trim()) : cleanNode(el).innerText.trim();
            download(new Blob([text], { type: 'text/plain;charset=utf-8' }), name + '.txt');
        }
    };

    function exportWord(element, filename) {
        const cleaned = cleanNode(element);
        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>${cleaned.innerHTML}</body></html>`;

        try {
            const blob = window.htmlDocx.asBlob(fullHtml);
            download(blob, filename + '.docx');
        } catch (e) { console.error(e); }
    }

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

    async function exportPDF(element, filename, btn) {
        try {
            const original = btn ? btn.textContent : '';
            if (btn) {
                btn.textContent = 'Wait...';
                btn.disabled = true;
            }

            await new Promise(resolve => setTimeout(resolve, 50));

            const cleaned = cleanNode(element);
            const container = document.createElement('div');
            container.style.cssText = `
                font-family: Arial, sans-serif;
                font-size: 14px;
                line-height: 1.6;
                padding: 20px;
                max-width: 800px;
                color: #000;
                background: #fff;
            `;
            container.appendChild(cleaned);

            container.style.position = 'absolute';
            container.style.left = '-9999px';
            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 imgData = canvas.toDataURL('image/jpeg', 0.85);
            const { jsPDF } = window.jspdf;
            const pdf = new jsPDF('p', 'mm', 'a4');

            const pdfWidth = pdf.internal.pageSize.getWidth();
            const pdfHeight = pdf.internal.pageSize.getHeight();
            const imgWidth = pdfWidth - 20;
            const imgHeight = (canvas.height * imgWidth) / canvas.width;

            let heightLeft = imgHeight;
            let position = 10;

            pdf.addImage(imgData, 'JPEG', 10, position, imgWidth, imgHeight);
            heightLeft -= (pdfHeight - 20);

            while (heightLeft > 0) {
                position = heightLeft - imgHeight + 10;
                pdf.addPage();
                pdf.addImage(imgData, 'JPEG', 10, position, imgWidth, imgHeight);
                heightLeft -= (pdfHeight - 20);
            }

            pdf.save(filename + '.pdf');

            if (btn) {
                btn.textContent = 'Done!';
                btn.disabled = false;
                setTimeout(() => btn.textContent = original, 2000);
            }
        } catch (e) {
            console.error('PDF export 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);
            const blobHtml = new Blob([cleaned.innerHTML], { type: 'text/html' });
            const blobText = new Blob([cleaned.innerText], { type: 'text/plain' });
            const data = [new ClipboardItem({ 'text/html': blobHtml, 'text/plain': blobText })];
            await navigator.clipboard.write(data);
            const original = btn.textContent;
            btn.textContent = 'Copied';
            setTimeout(() => btn.textContent = original, 2000);
        } catch (e) { console.error(e); }
    }

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

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

    function getCombinedHTML() {
        const container = document.createElement('div');
        const nodes = getCombinedNodes();
        nodes.forEach(node => {
            const isUser = node.matches(CONFIG.user);
            const wrapper = document.createElement('div');
            wrapper.style.marginBottom = '20px';
            if (isUser) {
                const h1 = document.createElement('h1');
                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;
    }

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

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

            const container = document.createElement('div');
            container.className = INJECTED_CLASS;
            Object.assign(container.style, COMMON_CONTAINER_STYLE);

            const name = generateFileName(null, index + 1);
            const nameAll = generateFileName(null, null);

            container.appendChild(createButton('Docx', () => exportWord(answer, name)));
            container.appendChild(createButton('Md', () => exportMarkdown(answer, name)));
            container.appendChild(createButton('Html', () => Exporters.html(answer, name)));
            container.appendChild(createButton('Pdf', (e) => exportPDF(answer, name, e.target)));
            container.appendChild(createButton('Json', () => Exporters.json(answer, name)));
            container.appendChild(createButton('Txt', () => Exporters.text(answer, name)));
            container.appendChild(createButton('Copy (Word)', (e) => copyContent(answer, e.target)));
            container.appendChild(createButton('Copy (Md)', (e) => copyMarkdownToClipboard(answer, e.target)));

            targetContainer.appendChild(container);

            if (index === answers.length - 1) {
                const allContainer = document.createElement('div');
                allContainer.className = INJECTED_CLASS + '-all';
                Object.assign(allContainer.style, COMMON_CONTAINER_STYLE);

                allContainer.appendChild(createButton('Docx All', () => exportWord(getCombinedHTML(), nameAll)));
                allContainer.appendChild(createButton('Md All', () => exportMarkdown(getCombinedHTML(), nameAll)));
                allContainer.appendChild(createButton('Html All', () => Exporters.html(getCombinedHTML(), nameAll)));
                allContainer.appendChild(createButton('Pdf All', (e) => exportPDF(getCombinedHTML(), nameAll, e.target)));
                allContainer.appendChild(createButton('Json All', () => Exporters.json(getCombinedNodes(), nameAll)));
                allContainer.appendChild(createButton('Txt All', () => Exporters.text(getCombinedHTML(), nameAll)));

                targetContainer.appendChild(allContainer);
            }
        });
    }

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

})();