ChatGPT.com Advanced Message Culler

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل 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);
})();