ChatGPT Chat Cloner

Converts ChatGPT chat into copyable text.

// ==UserScript==
// @name         ChatGPT Chat Cloner
// @namespace    https://WildPinkRice.github.io/
// @version      1.0
// @description  Converts ChatGPT chat into copyable text.
// @author       WildPinkRice
// @match        *://chatgpt.com/*
// @grant        GM_setClipboard
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // === Helper ===
    function el(tag, attrs = {}, children = []) {
        const e = document.createElement(tag);
        for (const k in attrs) {
            if (k === 'style') Object.assign(e.style, attrs[k]);
            else if (k.startsWith('on') && typeof attrs[k] === 'function') e.addEventListener(k.slice(2), attrs[k]);
            else e.setAttribute(k, attrs[k]);
        }
        children.forEach(c => typeof c === 'string' ? e.appendChild(document.createTextNode(c)) : e.appendChild(c));
        return e;
    }

    // === Main floating button ===
    const btn = el('button', {
        style: {
            position: 'fixed',
            bottom: '10px',
            right: '10px',
            background: '#10a37f',
            color: 'white',
            border: 'none',
            borderRadius: '8px',
            padding: '10px 14px',
            cursor: 'pointer',
            zIndex: '999999',
            fontSize: '14px',
            boxShadow: '0 0 10px rgba(0,0,0,0.3)'
        }
    }, ['Chat Cloner']);
    document.body.appendChild(btn);

    // === Popup container ===
    const popup = el('div', {
        style: {
            display: 'none',
            position: 'fixed',
            bottom: '60px',
            right: '10px',
            background: '#1e1e1e',
            color: 'white',
            padding: '12px',
            borderRadius: '10px',
            width: '380px',
            zIndex: '1000000',
            boxShadow: '0 0 20px rgba(0,0,0,0.6)',
            fontFamily: 'sans-serif',
            positionRelative: 'true'
        }
    });

    // === Title bar with HELP button ===
    const titleBar = el('div', {
        style: {
            display: 'flex',
            justifyContent: 'space-between',
            alignItems: 'center',
            marginBottom: '8px'
        }
    }, [
        el('h3', { style: { margin: 0, fontSize: '16px' } }, ['ChatGPT Chat Cloner']),
        el('button', {
            id: 'helpInside',
            style: {
                background: '#444',
                color: 'white',
                border: 'none',
                borderRadius: '6px',
                padding: '4px 8px',
                cursor: 'pointer',
                fontSize: '12px'
            }
        }, ['HELP'])
    ]);

    popup.appendChild(titleBar);

    // === Input & controls ===
    const chatUrlInput = el('input', {
        id: 'chatUrl',
        type: 'text',
        placeholder: 'Paste ChatGPT share link (optional)',
        style: {
            width: '100%',
            padding: '8px',
            border: '1px solid #333',
            borderRadius: '6px',
            marginBottom: '8px',
            background: '#fff',
            color: '#000'
        }
    });

    const buttonRow = el('div', { style: { display: 'flex', gap: '8px', marginBottom: '8px' } });
    const loadAutoBtn = el('button', {
        id: 'loadAuto',
        style: {
            flex: '1',
            padding: '8px',
            background: '#10a37f',
            border: 'none',
            borderRadius: '6px',
            color: 'white',
            cursor: 'pointer'
        }
    }, ['Load']);
    const loadManualBtn = el('button', {
        id: 'loadManual',
        style: {
            flex: '1',
            padding: '8px',
            background: '#0e8064',
            border: 'none',
            borderRadius: '6px',
            color: 'white',
            cursor: 'pointer'
        }
    }, ['Load with URL']);
    buttonRow.appendChild(loadAutoBtn);
    buttonRow.appendChild(loadManualBtn);

    const chatOutput = el('textarea', {
        id: 'chatOutput',
        style: {
            width: '100%',
            height: '260px',
            marginTop: '8px',
            padding: '8px',
            background: '#222',
            color: '#ddd',
            border: '1px solid #333',
            borderRadius: '6px',
            boxSizing: 'border-box',
            whiteSpace: 'pre-wrap'
        }
    });
    const copyBtn = el('button', {
        id: 'copyChat',
        style: {
            width: '100%',
            padding: '8px',
            marginTop: '8px',
            background: '#10a37f',
            border: 'none',
            borderRadius: '6px',
            color: 'white',
            cursor: 'pointer'
        }
    }, ['Copy to clipboard']);

    popup.appendChild(chatUrlInput);
    popup.appendChild(buttonRow);
    popup.appendChild(chatOutput);
    popup.appendChild(copyBtn);
    document.body.appendChild(popup);

    // === HELP Panel ===
    const helpPanel = el('div', {
        style: {
            display: 'none',
            position: 'fixed',
            bottom: '60px',
            right: '410px',
            background: '#111',
            color: '#eee',
            padding: '12px',
            borderRadius: '10px',
            width: '360px',
            zIndex: '1000000',
            boxShadow: '0 0 20px rgba(0,0,0,0.6)',
            fontFamily: 'sans-serif',
            fontSize: '13px'
        }
    });
    helpPanel.innerHTML = `
        <h3 style="margin:0 0 8px 0;font-size:16px;">Help — How to use</h3>
        <div style="line-height:1.4;">
            <strong>Autoloader note</strong>
            <p>The autoloader works best with short conversations and pages that are fully loaded. If you have a long conversation, scroll to the top and let the page finish loading all messages before using.</p>

            <strong>How to get a share URL</strong>
            <ol>
                <li>Open your ChatGPT conversation.</li>
                <li>Click the Share icon → Copy link.</li>
                <li>URL should look like this: <pre style="background:#222;padding:6px;border-radius:4px;">https://chatgpt.com/share/xxxxxxxxxxxxxxxx</pre></li>
            </ol>

            <strong>Manual Load</strong>
            <p>If auto-detection fails, paste the share link and click “Load with URL”.</p>

            <strong>Notes</strong>
            <ul>
                </strong><strong style="color: #ff5555;">URL loader does not work and if someone knows how to fix it please contact me.</strong>

            </ul>
        </div>
    `;
    document.body.appendChild(helpPanel);

    // === Event handlers ===
    btn.addEventListener('click', () => {
        popup.style.display = popup.style.display === 'none' ? 'block' : 'none';
        helpPanel.style.display = 'none';
    });

    document.getElementById('helpInside').addEventListener('click', () => {
        helpPanel.style.display = helpPanel.style.display === 'none' ? 'block' : 'none';
    });

    chatUrlInput.addEventListener('focus', () => chatUrlInput.select());

    // === Clipboard ===
    document.getElementById('copyChat').addEventListener('click', () => {
        const text = chatOutput.value.trim();
        if (!text) return alert('Nothing to copy.');
        GM_setClipboard(text);
        alert('Chat copied to clipboard.');
    });

    // === Text cleaning ===
    function cleanText(t) {
        if (!t) return '';
        return t
            .replace(/window\.__oai[^\n]*/gi, '')
            .replace(/<script[\s\S]*?<\/script>/gi, '')
            .replace(/<\/?[^>]+>/g, '')
            .replace(/\n{3,}/g, '\n\n')
            .trim();
    }

    // === Extractors ===
    async function extractChatFromHTML(html) {
        const match = html.match(/<script id="__NEXT_DATA__" type="application\/json">([\s\S]+?)<\/script>/);
        if (match) {
            const data = JSON.parse(match[1]);
            const messages = data.props?.pageProps?.sharedConversation?.messages;
            if (messages?.length)
                return messages.map(m =>
                    `${m.author?.role === 'assistant' ? 'ChatGPT' : 'User'}:\n${cleanText(m.content?.[0]?.text?.value || '')}`
                ).join('\n\n');
        }
        return null;
    }

    function extractFromDOM() {
        const msgs = document.querySelectorAll('[data-message-author-role]');
        if (!msgs.length) return null;
        return Array.from(msgs).map(el => {
            const role = el.getAttribute('data-message-author-role') === 'assistant' ? 'ChatGPT' : 'You';
            return `${role}:\n${cleanText(el.innerText)}`;
        }).join('\n\n');
    }

    async function loadFromURL(url) {
        chatOutput.value = 'Loading...';
        try {
            const html = await (await fetch(url)).text();
            const text = await extractChatFromHTML(html);
            chatOutput.value = text || 'Could not find chat data.';
        } catch (e) {
            chatOutput.value = 'Error: ' + e.message;
        }
    }

    async function loadAuto() {
        chatOutput.value = 'Detecting...';
        const url = window.location.href;
        if (url.includes('/share/')) return loadFromURL(url);
        const text = extractFromDOM();
        chatOutput.value = text || 'No chat found or not fully loaded.';
    }

    document.getElementById('loadManual').addEventListener('click', () => {
        const url = chatUrlInput.value.trim();
        if (!url) return alert('Please paste a ChatGPT share link first.');
        loadFromURL(url);
    });
    document.getElementById('loadAuto').addEventListener('click', loadAuto);
})();