ChatGPT.com Advanced Message Culler

Culls messages from ChatGPT conversations via an overlay. Helpful for those who have low-end hardwares

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         ChatGPT.com Advanced Message Culler
// @namespace    https://chatgpt.com/
// @version      1.5.0
// @description  Culls messages from ChatGPT conversations via an overlay. Helpful for those who have low-end hardwares
// @author       sytesn, ChatGPT
// @match        https://chatgpt.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const PIN_STORAGE_KEY = 'chatgpt_message_pins';
    const SETTINGS_KEYS = [
        'culler-limit',
        'culler-enabled',
        'culler-mode',
        'culler-fade',
        'panel-left',
        'panel-top'
    ];

    const getPinnedIds = () => JSON.parse(localStorage.getItem(PIN_STORAGE_KEY) || '[]');
    const setPinnedIds = (ids) => localStorage.setItem(PIN_STORAGE_KEY, JSON.stringify(ids));

    let MESSAGE_LIMIT = parseInt(localStorage.getItem('culler-limit')) || 50;
    let cullingEnabled = localStorage.getItem('culler-enabled') === 'true';
    let mode = localStorage.getItem('culler-mode') || "hide";
    let fadeEnabled = localStorage.getItem('culler-fade') !== 'false';
    let multiSelect = false;

    const style = document.createElement('style');
    style.textContent = `
        #culler-panel {
            position: fixed;
            top: ${localStorage.getItem('panel-top') || '10px'};
            left: ${localStorage.getItem('panel-left') || '10px'};
            z-index: 9999;
            background-color: #111;
            padding: 10px;
            border-radius: 8px;
            font-family: Arial, sans-serif;
            font-size: 13px;
            color: #fff;
            transition: opacity 0.3s ease;
            max-width: 300px;
            resize: both;
            overflow: auto;
        }
        .fade {
            transition: opacity 0.4s ease;
            opacity: 0;
        }
        .fade-in {
            opacity: 1 !important;
        }
        #culler-panel input, #culler-panel select {
            background-color: #222;
            color: #fff;
            border: 1px solid #444;
            border-radius: 4px;
            padding: 2px 4px;
        }
        #culler-panel button {
            margin-top:6px;
            padding:4px 8px;
            border: none;
            border-radius: 4px;
            background-color: #444;
            color: #fff;
            cursor: pointer;
        }
        .top-right-btn {
            position: absolute;
            top: 4px;
            right: 6px;
            background: transparent;
            font-size: 14px;
            padding: 0;
        }
        .pin-btn {
            position: absolute;
            top: 4px;
            right: 4px;
            background: #444;
            border: none;
            color: #fff;
            border-radius: 4px;
            padding: 2px 6px;
            font-size: 12px;
            cursor: pointer;
        }
        #pin-panel {
            position: fixed;
            top: 60px;
            right: 10px;
            width: 300px;
            max-height: 70vh;
            overflow-y: auto;
            background: #1e1e1e;
            color: white;
            border-radius: 8px;
            padding: 10px;
            z-index: 9999;
            box-shadow: 0 0 10px #000;
            display: none;
        }
        .pin-message {
            background: #2e2e2e;
            border-radius: 6px;
            padding: 8px;
            margin-bottom: 6px;
            font-size: 13px;
            position: relative;
        }
        .pin-message button {
            position: absolute;
            top: 4px;
            right: 4px;
            background: #900;
            border: none;
            color: white;
            border-radius: 4px;
            padding: 2px 6px;
            font-size: 12px;
            cursor: pointer;
        }
    `;
    document.head.appendChild(style);

    const panel = document.createElement('div');
    panel.id = 'culler-panel';
    panel.innerHTML = `
        <button class="top-right-btn" id="overlay-minimize">✕</button>
        <div style="margin-bottom:6px;font-weight:bold;">Message Culler</div>
        <label><input type="checkbox" id="culler-enable"> Enable</label><br>
        <label>Keep last <input type="number" id="culler-limit" value="${MESSAGE_LIMIT}" style="width: 50px;"></label><br>
        <label>Mode:
            <select id="culler-mode">
                <option value="hide">Hide</option>
                <option value="remove">Remove</option>
            </select>
        </label><br>
        <label>Fade: <input type="checkbox" id="culler-fade" ${fadeEnabled ? 'checked' : ''}></label><br>
        <button id="culler-refresh">Refresh Now</button>
        <button id="pin-toggle">📌 View Pins</button><br>
        <button id="multi-select-toggle">Multi-Select: Off</button>
        <button id="export-btn">Export</button>
        <button id="import-btn">Import</button>
    `;
    document.body.appendChild(panel);

    // Draggable support
    (function makeDraggable(el) {
        let isDragging = false, offsetX = 0, offsetY = 0;
        el.addEventListener('mousedown', (e) => {
            if (["INPUT", "SELECT", "BUTTON"].includes(e.target.tagName)) return;
            isDragging = true;
            offsetX = e.clientX - el.offsetLeft;
            offsetY = e.clientY - el.offsetTop;
        });
        document.addEventListener('mousemove', (e) => {
            if (isDragging) {
                el.style.left = (e.clientX - offsetX) + 'px';
                el.style.top = (e.clientY - offsetY) + 'px';
                localStorage.setItem('panel-left', el.style.left);
                localStorage.setItem('panel-top', el.style.top);
            }
        });
        document.addEventListener('mouseup', () => isDragging = false);
    })(panel);

    const enableCheckbox = panel.querySelector('#culler-enable');
    const limitInput = panel.querySelector('#culler-limit');
    const modeSelect = panel.querySelector('#culler-mode');
    const fadeCheckbox = panel.querySelector('#culler-fade');
    const refreshBtn = panel.querySelector('#culler-refresh');
    const multiToggle = panel.querySelector('#multi-select-toggle');

    enableCheckbox.checked = cullingEnabled;
    modeSelect.value = mode;

    enableCheckbox.addEventListener('change', () => {
        cullingEnabled = enableCheckbox.checked;
        localStorage.setItem('culler-enabled', cullingEnabled);
        applyCulling();
    });

    limitInput.addEventListener('change', () => {
        const oldLimit = MESSAGE_LIMIT;
        MESSAGE_LIMIT = parseInt(limitInput.value, 10) || 50;
        localStorage.setItem('culler-limit', MESSAGE_LIMIT);
        applyCulling(oldLimit);
    });

    modeSelect.addEventListener('change', () => {
        mode = modeSelect.value;
        localStorage.setItem('culler-mode', mode);
        applyCulling();
    });

    fadeCheckbox.addEventListener('change', () => {
        fadeEnabled = fadeCheckbox.checked;
        localStorage.setItem('culler-fade', fadeEnabled);
    });

    refreshBtn.addEventListener('click', () => applyCulling());

    multiToggle.addEventListener('click', () => {
        multiSelect = !multiSelect;
        multiToggle.textContent = `Multi-Select: ${multiSelect ? 'On' : 'Off'}`;
    });

    document.getElementById('overlay-minimize').addEventListener('click', () => {
        panel.style.display = 'none';
        showRestoreBtn();
    });

    function showRestoreBtn() {
        const btn = document.createElement('button');
        btn.textContent = 'Show Overlay';
        btn.style = 'position:fixed;top:10px;left:10px;z-index:9999;background:#333;color:#fff;padding:6px;border:none;border-radius:4px;';
        btn.onclick = () => {
            panel.style.display = 'block';
            btn.remove();
        };
        document.body.appendChild(btn);
    }

    document.getElementById('export-btn').addEventListener('click', () => {
        const data = {};
        SETTINGS_KEYS.forEach(k => data[k] = localStorage.getItem(k));
        data.pins = getPinnedIds();
        const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.download = 'chatgpt_culler_backup.json';
        a.href = url;
        a.click();
        URL.revokeObjectURL(url);
    });

    document.getElementById('import-btn').addEventListener('click', () => {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json';
        input.onchange = e => {
            const file = e.target.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onload = e => {
                try {
                    const data = JSON.parse(e.target.result);
                    SETTINGS_KEYS.forEach(k => {
                        if (data[k] !== undefined) localStorage.setItem(k, data[k]);
                    });
                    if (Array.isArray(data.pins)) setPinnedIds(data.pins);
                    location.reload();
                } catch (e) {
                    alert('Invalid file!');
                }
            };
            reader.readAsText(file);
        };
        input.click();
    });

    // Command palette
    document.addEventListener('keydown', (e) => {
        if (e.ctrlKey && e.shiftKey && e.key === 'K') {
            const cmd = prompt('Culler Command: hide/show/refresh');
            if (cmd === 'hide') panel.style.display = 'none';
            if (cmd === 'show') panel.style.display = 'block';
            if (cmd === 'refresh') applyCulling();
        }
    });

    const pinPanel = document.createElement('div');
    pinPanel.id = 'pin-panel';
    pinPanel.innerHTML = `<h2>Pinned Messages</h2><div id="pin-container"></div>`;
    document.body.appendChild(pinPanel);

    document.getElementById('pin-toggle').addEventListener('click', () => {
        pinPanel.style.display = pinPanel.style.display === 'none' ? 'block' : 'none';
        updatePinnedPanel();
    });

    function getMessageElements() {
        return Array.from(document.querySelectorAll('article[data-testid^="conversation-turn-"]'));
    }

    function applyCulling(previousLimit = MESSAGE_LIMIT) {
        const messages = getMessageElements();
        const pinned = new Set(getPinnedIds());
        const visible = messages.filter(msg => !pinned.has(msg.dataset.testid));
        const limitCutoff = visible.length - MESSAGE_LIMIT;

        messages.forEach((msg, i) => {
            const id = msg.dataset.testid;
            const isPinned = pinned.has(id);
            const visibleIndex = visible.indexOf(msg);

            ensurePinButton(msg);

            if (!cullingEnabled || isPinned || visibleIndex >= limitCutoff) {
                if (fadeEnabled && previousLimit < MESSAGE_LIMIT && visibleIndex >= limitCutoff && visibleIndex < visible.length - previousLimit) {
                    msg.classList.add('fade');
                    msg.style.display = '';
                    setTimeout(() => msg.classList.add('fade-in'), 10);
                    setTimeout(() => msg.classList.remove('fade', 'fade-in'), 410);
                } else {
                    msg.style.display = '';
                }
                msg.classList.remove('culled');
            } else {
                if (mode === "remove") {
                    if (!msg.dataset.removed) {
                        msg.dataset.removed = "true";
                        msg.remove();
                    }
                } else {
                    if (fadeEnabled) {
                        msg.classList.add('fade');
                        msg.classList.remove('fade-in');
                        setTimeout(() => msg.style.display = 'none', 400);
                    } else {
                        msg.style.display = "none";
                    }
                    msg.classList.add("culled");
                }
            }
        });
    }

    function ensurePinButton(msg) {
        if (msg.querySelector('.pin-btn')) return;
        const btn = document.createElement('button');
        btn.className = 'pin-btn';
        const id = msg.dataset.testid;
        const pinned = new Set(getPinnedIds());
        btn.textContent = pinned.has(id) ? 'Unpin' : 'Pin';

        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            const ids = new Set(getPinnedIds());

            if (multiSelect) {
                const allVisible = getMessageElements().filter(el => el.style.display !== 'none');
                allVisible.forEach(el => {
                    const elId = el.dataset.testid;
                    if (!ids.has(elId)) ids.add(elId);
                });
            } else {
                if (ids.has(id)) ids.delete(id); else ids.add(id);
            }

            setPinnedIds([...ids]);
            applyCulling();
        });

        msg.style.position = 'relative';
        msg.appendChild(btn);
    }

    function updatePinnedPanel() {
        const pinned = new Set(getPinnedIds());
        const container = document.getElementById('pin-container');
        container.innerHTML = '';
        getMessageElements().forEach(msg => {
            if (pinned.has(msg.dataset.testid)) {
                const clone = msg.cloneNode(true);
                clone.querySelector('.pin-btn')?.remove();
                const id = msg.dataset.testid;
                const unpinBtn = document.createElement('button');
                unpinBtn.textContent = 'Unpin';
                unpinBtn.onclick = () => {
                    const ids = new Set(getPinnedIds());
                    ids.delete(id);
                    setPinnedIds([...ids]);
                    updatePinnedPanel();
                    applyCulling();
                };
                clone.appendChild(unpinBtn);
                clone.className = 'pin-message';
                container.appendChild(clone);
            }
        });
    }

    new MutationObserver(() => {
        if (cullingEnabled) applyCulling();
    }).observe(document.body, { childList: true, subtree: true });

    setTimeout(() => applyCulling(), 1000);
})();