Simple ChatGPT Text Exporter

Logs ChatGPT messages with labels, dynamically updates, and includes a copy button. UI can be positioned at the top center or above the input box.

Устаревшая версия за 17.10.2024. Перейдите к последней версии.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Simple ChatGPT Text Exporter
// @namespace    https://github.com/samomar/Simple-ChatGPT-Text-Exporter
// @version      4.2
// @description  Logs ChatGPT messages with labels, dynamically updates, and includes a copy button. UI can be positioned at the top center or above the input box.
// @match        https://chatgpt.com/*
// @grant        none
// @homepage     https://github.com/samomar/Simple-ChatGPT-Text-Exporter
// @supportURL   https://github.com/samomar/Simple-ChatGPT-Text-Exporter/issues
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        enableLogging: false,
        chatContainerSelector: localStorage.getItem('chatContainerSelector') || '',
        position: localStorage.getItem('chatLoggerPosition') || 'bottom'
    };

    let chatMessages = [];
    let observer = null;
    let lastUrl = location.href;
    let chatData = null;

    // Intercept the network requests
    const originalFetch = window.fetch;
    window.fetch = function(...args) {
        return originalFetch.apply(this, args).then(async (response) => {
            const url = response.url;
            if (url.includes('conversation')) {
                const clonedResponse = response.clone();
                const jsonData = await clonedResponse.json();
                if (jsonData.mapping) {
                    chatData = jsonData;
                    updateChatMessages();
                }
            }
            return response;
        });
    };

    function init() {
        resetChatData();
        createControls();
        if (CONFIG.chatContainerSelector) {
            observeChatContainer(CONFIG.chatContainerSelector);
        } else {
            const containers = findPossibleChatContainers();
            if (containers.length > 0) {
                CONFIG.chatContainerSelector = containers[0].selector;
                localStorage.setItem('chatContainerSelector', CONFIG.chatContainerSelector);
                observeChatContainer(CONFIG.chatContainerSelector);
            }
        }
    }

    function resetChatData() {
        chatMessages = [];
        if (observer) {
            observer.disconnect();
            observer = null;
        }
    }

    function createControls() {
        const existingControls = document.getElementById('chat-logger-controls');
        if (existingControls) existingControls.remove();

        const container = document.createElement('div');
        container.id = 'chat-logger-controls';

        updateControlsStyle(container);

        container.innerHTML = `
            <button id="toggle-selector-button" class="chat-logger-btn">⚙️</button>
            <div class="dropdown">
                <button id="download-chat-button" class="chat-logger-btn">⬇️</button>
                <div class="dropdown-content">
                    <a href="#" id="download-txt">Download TXT</a>
                    <a href="#" id="download-json">Download JSON</a>
                </div>
            </div>
            <button id="copy-chat-button" class="chat-logger-btn">Copy Chat</button>
            <div id="chat-selector-container" style="display:none;">
                <select id="chat-container-dropdown" class="chat-logger-select"></select>
                <button id="copy-selector-button" class="chat-logger-btn">📋</button>
                <button id="toggle-position-button" class="chat-logger-btn">↕️</button>
            </div>
        `;

        if (!document.getElementById('chat-logger-style')) {
            const style = document.createElement('style');
            style.id = 'chat-logger-style';
            style.textContent = `
                #chat-logger-controls {
                    display: flex;
                    align-items: center;
                    gap: 5px;
                    padding: 5px;
                    background-color: #202123;
                    border-radius: 5px;
                }
                .chat-logger-btn {
                    padding: 5px 10px;
                    font-size: 12px;
                    background-color: #343541;
                    color: #fff;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                }
                .chat-logger-btn:hover {
                    background-color: #40414f;
                }
                .chat-logger-select {
                    background-color: #343541;
                    color: #fff;
                    border: none;
                    border-radius: 4px;
                    padding: 5px;
                    font-size: 12px;
                }
                .dropdown {
                    position: relative;
                    display: inline-block;
                }
                .dropdown-content {
                    display: none;
                    position: absolute;
                    background-color: #202123;
                    min-width: 160px;
                    box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
                    z-index: 1;
                    border-radius: 4px;
                    top: 100%;
                    left: 0;
                }
                .dropdown-content a {
                    color: #fff;
                    padding: 12px 16px;
                    text-decoration: none;
                    display: block;
                    font-size: 12px;
                }
                .dropdown-content a:hover {
                    background-color: #343541;
                }
                .dropdown:hover .dropdown-content {
                    display: block;
                }
            `;
            document.head.appendChild(style);
        }

        if (CONFIG.position === 'top') {
            document.body.insertBefore(container, document.body.firstChild);
        } else {
            const targetElement = document.querySelector('.flex.w-full.flex-col.gap-1\\.5.rounded-\\[26px\\].p-1\\.5.transition-colors.contain-inline-size.bg-\\[\\#f4f4f4\\].dark\\:bg-token-main-surface-secondary');
            if (targetElement && targetElement.parentElement) {
                const wrapper = document.createElement('div');
                wrapper.style.cssText = 'display:flex;flex-direction:column;width:100%;';
                targetElement.parentElement.insertBefore(wrapper, targetElement);
                wrapper.appendChild(container);
                wrapper.appendChild(targetElement);
            } else {
                document.body.appendChild(container);
            }
        }

        populateDropdown();
        addEventListeners();
    }

    function updateControlsStyle(container) {
        const commonStyles = `
            z-index: 9999;
            background-color: rgba(0, 0, 0, 0.3);
            border: 1px solid rgba(255, 255, 255, 0.1);
            font-family: Arial, sans-serif;
            color: #fff;
            border-radius: 4px;
            display: flex;
            align-items: center;
            padding: 3px 6px;
            font-size: 12px;
            gap: 4px;
            margin-bottom: 10px;
            width: fit-content;
        `;

        if (CONFIG.position === 'top') {
            container.style.cssText = `
                ${commonStyles}
                position: fixed;
                top: 10px;
                left: 50%;
                transform: translateX(-50%);
            `;
        } else {
            container.style.cssText = commonStyles;
        }
    }

    function addEventListeners() {
        const controls = document.getElementById('chat-logger-controls');
        controls.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
        });

        document.getElementById('toggle-selector-button').addEventListener('click', toggleSelectorVisibility);
        document.getElementById('download-chat-button').addEventListener('click', toggleDownloadOptions);
        document.getElementById('download-txt').addEventListener('click', (e) => downloadChat(e, 'txt'));
        document.getElementById('download-json').addEventListener('click', (e) => downloadChat(e, 'json'));
        document.getElementById('copy-chat-button').addEventListener('click', copyChat);
        document.getElementById('toggle-position-button').addEventListener('click', togglePosition);
        document.getElementById('copy-selector-button').addEventListener('click', copySelectorToClipboard);
        document.getElementById('chat-container-dropdown').addEventListener('change', onSelectChange);

        document.addEventListener('click', closeDropdowns);
    }

    function toggleSelectorVisibility(e) {
        e.preventDefault();
        const selectorContainer = document.getElementById('chat-selector-container');
        selectorContainer.style.display = selectorContainer.style.display === 'none' ? 'block' : 'none';
    }

    function toggleDownloadOptions(e) {
        e.preventDefault();
        const dropdownContent = document.querySelector('.dropdown-content');
        dropdownContent.style.display = dropdownContent.style.display === 'none' ? 'block' : 'none';
    }

    function copyChat(e) {
        e.preventDefault();
        const button = e.target;
        const originalText = button.innerText;

        // If the button is already in an active state, do nothing
        if (button.dataset.active === 'true') {
            return;
        }

        const chatContent = chatMessages.join('\n\n');
        button.dataset.active = 'true';

        if (chatContent.trim()) {
            navigator.clipboard.writeText(chatContent).then(() => {
                showTemporaryStatus(button, 'Copied!', '#4CAF50');
            }).catch(() => {
                showTemporaryStatus(button, 'Failed to Copy', '#f44336');
            }).finally(() => {
                // Ensure the button always reverts to its original state
                setTimeout(() => {
                    button.innerText = originalText;
                    button.style.backgroundColor = '';
                    button.dataset.active = 'false';
                }, 2000);
            });
        } else {
            showTemporaryStatus(button, 'Please wait for chat to load', '#FFA500');
            // Revert to original state after the temporary message
            setTimeout(() => {
                button.innerText = originalText;
                button.style.backgroundColor = '';
                button.dataset.active = 'false';
            }, 2000);
        }
    }

    function downloadChat(e, format) {
        e.preventDefault();
        const content = format === 'json' ? JSON.stringify(chatMessages, null, 2) : chatMessages.join('\n\n');
        const blob = new Blob([content], { type: format === 'json' ? 'application/json' : 'text/plain' });
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        const fileName = document.title.replace(/[^a-z0-9]/gi, '_').toLowerCase() || 'chat_export';
        a.download = `${fileName}.${format}`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(a.href);
    }

    function togglePosition(e) {
        e.preventDefault();
        CONFIG.position = CONFIG.position === 'top' ? 'bottom' : 'top';
        localStorage.setItem('chatLoggerPosition', CONFIG.position);
        createControls();
    }

    function copySelectorToClipboard(e) {
        e.preventDefault();
        const select = document.getElementById('chat-container-dropdown');
        navigator.clipboard.writeText(select.value).then(() => {
            alert('Selector copied to clipboard!');
        }).catch(() => {
            alert('Failed to copy selector');
        });
    }

    function onSelectChange(e) {
        e.preventDefault();
        CONFIG.chatContainerSelector = e.target.value;
        localStorage.setItem('chatContainerSelector', CONFIG.chatContainerSelector);
        resetChatData();
        if (CONFIG.chatContainerSelector) {
            observeChatContainer();
        }
    }

    function showTemporaryStatus(button, message, bgColor) {
        button.innerText = message;
        button.style.backgroundColor = bgColor;
    }

    function closeDropdowns() {
        document.querySelectorAll('.dropdown-content, #chat-selector-container').forEach(el => {
            el.style.display = 'none';
        });
    }

    function checkUrlChange() {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            resetCopyButton();
            init(); // Fully reinitialize on URL change
        }
    }

    function resetCopyButton() {
        const copyButton = document.getElementById('copy-chat-button');
        if (copyButton) {
            copyButton.innerText = 'Copy Chat';
            copyButton.style.backgroundColor = '';
            copyButton.dataset.active = 'false';
        }
    }

    function handlePageChanges() {
        const controlPanel = document.getElementById('chat-logger-controls');
        if (!controlPanel) {
            init();
        } else {
            // Ensure chat container is still being observed
            if (CONFIG.chatContainerSelector) {
                observeChatContainer(CONFIG.chatContainerSelector);
            }
        }
    }

    function observeChatContainer(selector) {
        if (observer) observer.disconnect();
        const container = document.querySelector(selector);
        if (container) {
            scanChatContent(container);
            observer = new MutationObserver(() => scanChatContent(container));
            observer.observe(container, { childList: true, subtree: true });
        }
    }

    function scanChatContent() {
        updateChatMessages();
    }

    function updateChatMessages() {
        if (!chatData || !chatData.mapping) return;

        const messages = [];
        const sortedNodes = Object.values(chatData.mapping).sort((a, b) => {
            return (a.message?.create_time || 0) - (b.message?.create_time || 0);
        });

        for (const node of sortedNodes) {
            if (node.message && node.message.content && node.message.content.parts) {
                const role = node.message.author.role;
                const content = node.message.content.parts.join('\n');
                if (content.trim()) {
                    if (role === 'user') {
                        messages.push(`You said:\n${content}`);
                    } else if (role === 'assistant') {
                        messages.push(`Assistant said:\n${content}`);
                    }
                    // Ignore system messages or other roles
                }
            }
        }

        chatMessages = messages;
        if (CONFIG.enableLogging) {
            console.log(chatMessages);
        }
    }

    function populateDropdown() {
        const select = document.getElementById('chat-container-dropdown');
        const options = findPossibleChatContainers();
        let optionsHTML = '<option value="">-- Select --</option>';
        options.forEach(opt => {
            optionsHTML += `<option value="${opt.selector}">${opt.description}</option>`;
        });
        select.innerHTML = optionsHTML;
        if (CONFIG.chatContainerSelector) select.value = CONFIG.chatContainerSelector;
    }

    function findPossibleChatContainers() {
        const selectors = [
            '[data-testid^="conversation-turn-"]',
            '[role*="log"]',
            '[role*="feed"]',
            '[role*="list"]',
            '[aria-live="polite"]',
            '[aria-relevant="additions"]',
            '[class*="chat"]',
            '[class*="message"]',
            'main',
            'section',
            'div[class*="conversation"]',
            'div[class*="thread"]',
            'div[class*="dialog"]'
        ];

        const seenSelectors = new Set();
        const result = [];

        selectors.forEach(selector => {
            document.querySelectorAll(selector).forEach(el => {
                const uniqueSelector = getUniqueSelector(el);
                if (!seenSelectors.has(uniqueSelector)) {
                    seenSelectors.add(uniqueSelector);
                    result.push({
                        selector: uniqueSelector,
                        description: buildElementDescription(el)
                    });
                }
            });
        });

        return result;
    }

    function getUniqueSelector(el) {
        if (el.id) return `#${el.id}`;
        if (el.classList && el.classList.length > 0) {
            const className = '.' + Array.from(el.classList).join('.');
            return `${el.tagName.toLowerCase()}${className}`;
        }
        return el.tagName.toLowerCase();
    }

    function buildElementDescription(el) {
        const description = [];
        if (el.id) {
            description.push(`#${el.id}`);
        }
        if (el.classList && el.classList.length > 0) {
            description.push(`.${Array.from(el.classList).join('.')}`);
        }
        description.push(el.tagName.toLowerCase());
        return description.join(' ');
    }

    function findInputBox() {
        const selectors = [
            'textarea',
            'div[contenteditable="true"]',
            'input[type="text"]',
            'div.group.relative.flex.w-full.items-center',
            'form div.relative',
            'div[role="presentation"]',
            'div.flex.flex-col.w-full.py-2.flex-grow.md\\:py-3.md\\:pl-4',
            'div.flex.flex-col.w-full.py-[10px].flex-grow.md\\:py-4.md\\:pl-4'
        ];
        for (const sel of selectors) {
            const el = document.querySelector(sel);
            if (el) return el;
        }
        return null;
    }

    // Set up observers for page changes
    const bodyObserver = new MutationObserver((mutations) => {
        for (let mutation of mutations) {
            if (mutation.type === 'childList') {
                handlePageChanges();
                break;
            }
        }
    });

    // Wait for the page to be fully loaded before initializing
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initAfterLoad);
    } else {
        initAfterLoad();
    }

    function initAfterLoad() {
        // Wait a short time after load to ensure all dynamic content is rendered
        setTimeout(() => {
            init();
            bodyObserver.observe(document.body, { childList: true, subtree: true });
            setInterval(checkUrlChange, 1000);
        }, 1000);
    }
})();