ChatGPT Bulk Deleter ✨

The ultimate tool for deleting ChatGPT conversations. Features a premium UI with enhanced shadows, icons, and a selection cursor. No pop-ups.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         ChatGPT Bulk Deleter ✨
// @namespace    http://tampermonkey.net/
// @version      6.0.0
// @description  The ultimate tool for deleting ChatGPT conversations. Features a premium UI with enhanced shadows, icons, and a selection cursor. No pop-ups.
// @author       @SavitarStorm @Tano (Deluxe Edition by Gemini)
// @match        https://chatgpt.com/*
// @connect      chatgpt.com
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        /* Container fits perfectly into sidebar */
        .bulk-delete-controls {
            padding: 8px 12px;
            display: flex;
            flex-direction: column;
            gap: 8px;
            border-bottom: 1px solid rgba(255,255,255,0.1);
            margin-bottom: 4px;
            background: transparent;
        }

        /* Stealth Buttons (Matches ChatGPT UI) */
        .stealth-btn {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 100%;
            padding: 8px 12px;
            border: 1px solid rgba(255,255,255,0.1);
            border-radius: 6px;
            cursor: pointer;
            font-size: 13px;
            font-weight: 400;
            color: #ececf1;
            background: transparent; /* Seamless blend */
            transition: background 0.2s ease;
        }
        .stealth-btn:hover { background: rgba(255,255,255,0.08); }
        .stealth-btn:active { opacity: 0.8; }
        .stealth-btn:disabled { opacity: 0.4; cursor: not-allowed; }

        /* Delete Button (Subtle Red) */
        .danger-btn {
            border-color: rgba(185, 28, 28, 0.5);
            color: #fca5a5;
            background: rgba(50, 20, 20, 0.3);
        }
        .danger-btn:hover { background: rgba(127, 29, 29, 0.4); }

        /* Active State */
        .active-mode {
            background: rgba(255,255,255,0.15);
            color: #fff;
            border-color: rgba(255,255,255,0.3);
        }

        /* Search Input */
        .search-wrapper {
            position: relative;
            display: none; /* Hidden by default */
        }
        .stealth-input {
            width: 100%;
            padding: 6px 10px 6px 28px;
            border-radius: 5px;
            border: 1px solid rgba(255,255,255,0.1);
            background: rgba(0,0,0,0.3);
            color: #fff;
            font-size: 13px;
            outline: none;
        }
        .stealth-input:focus { border-color: rgba(255,255,255,0.3); }
        .search-icon {
            position: absolute;
            left: 8px;
            top: 50%;
            transform: translateY(-50%);
            font-size: 12px;
            opacity: 0.5;
        }

        /* Action Row */
        .action-row { display: flex; gap: 6px; display: none; }
        .action-row > button { flex: 1; }

        /* --- INTERACTION & ANIMATION --- */

        /* 1. Selection Cursor (Plus) */
        .chat-selectable { cursor: cell !important; }

        /* 2. Selected Highlight */
        a.chat-selected {
            background: rgba(255, 255, 255, 0.08) !important;
            border-left: 3px solid #ef4444 !important; /* Red selection marker */
        }

        /* 3. Fire Animation */
        @keyframes burnEffect {
            0% { filter: brightness(1); transform: scale(1); }
            30% { filter: brightness(2) sepia(1) hue-rotate(-10deg); transform: scale(1.02); }
            100% { filter: grayscale(1) brightness(0); opacity: 0; transform: scale(0.9) translateX(-10px); }
        }
        .burning {
            animation: burnEffect 0.5s forwards ease-in-out;
            pointer-events: none;
        }
    `);

    let selectionMode = false;
    const selectedChats = new Set();
    let authToken = null;

    // --- Auth ---
    async function getAuthToken() {
        if (authToken) return authToken;
        try {
            const response = await fetch("https://chatgpt.com/api/auth/session");
            if (!response.ok) throw new Error("Auth Failed");
            const data = await response.json();
            authToken = data.accessToken;
            return authToken;
        } catch (e) {
            console.error("Auth Error", e);
            return null;
        }
    }

    // --- Init ---
    function initialize() {
        const headerDiv = document.querySelector('#sidebar-header');
        if (!headerDiv || document.getElementById('toggle-select-btn')) return;

        const controls = document.createElement('div');
        controls.className = 'bulk-delete-controls';

        // 1. Toggle
        const toggleBtn = document.createElement('button');
        toggleBtn.id = 'toggle-select-btn';
        toggleBtn.className = 'stealth-btn';
        toggleBtn.textContent = 'Select Chats';
        toggleBtn.onclick = toggleSelectionMode;

        // 2. Search
        const searchDiv = document.createElement('div');
        searchDiv.className = 'search-wrapper';
        searchDiv.innerHTML = '<span class="search-icon">🔍</span>';
        const searchInput = document.createElement('input');
        searchInput.className = 'stealth-input';
        searchInput.placeholder = 'Search...';
        searchInput.oninput = handleSearch;
        searchDiv.appendChild(searchInput);

        // 3. Actions
        const row = document.createElement('div');
        row.className = 'action-row';

        const allBtn = document.createElement('button');
        allBtn.className = 'stealth-btn';
        allBtn.textContent = 'All';
        allBtn.onclick = selectAllChats;

        const noneBtn = document.createElement('button');
        noneBtn.className = 'stealth-btn';
        noneBtn.textContent = 'None';
        noneBtn.onclick = deselectAllChats;

        row.append(allBtn, noneBtn);

        // 4. Delete
        const delBtn = document.createElement('button');
        delBtn.id = 'del-btn';
        delBtn.className = 'stealth-btn danger-btn';
        delBtn.innerHTML = '🔥 Delete';
        delBtn.style.display = 'none';
        delBtn.disabled = true;
        delBtn.onclick = deleteSelectedChats;

        controls.append(toggleBtn, searchDiv, row, delBtn);
        headerDiv.parentElement.appendChild(controls);
    }

    // --- Logic ---
    function toggleSelectionMode() {
        selectionMode = !selectionMode;
        const toggleBtn = document.getElementById('toggle-select-btn');
        const hiddenEls = [
            document.querySelector('.search-wrapper'),
            document.querySelector('.action-row'),
            document.getElementById('del-btn')
        ];
        const chatItems = document.querySelectorAll('a[href^="/c/"]');

        if (selectionMode) {
            toggleBtn.textContent = 'Cancel';
            toggleBtn.classList.add('active-mode');
            hiddenEls.forEach(el => el.style.display = el.classList.contains('action-row') ? 'flex' : 'block');

            chatItems.forEach(chat => {
                chat.classList.add('chat-selectable');
                chat.addEventListener('click', handleChatClick, true);
            });
        } else {
            toggleBtn.textContent = 'Select Chats';
            toggleBtn.classList.remove('active-mode');
            hiddenEls.forEach(el => el.style.display = 'none');

            // Reset search
            document.querySelector('.stealth-input').value = '';
            handleSearch({target: {value: ''}});

            chatItems.forEach(chat => {
                chat.classList.remove('chat-selectable', 'chat-selected');
                chat.removeEventListener('click', handleChatClick, true);
            });
            selectedChats.clear();
            updateDelBtn();
        }
    }

    function handleSearch(e) {
        const q = e.target.value.toLowerCase();
        document.querySelectorAll('a[href^="/c/"]').forEach(chat => {
            chat.style.display = chat.textContent.toLowerCase().includes(q) ? 'flex' : 'none';
        });
    }

    function handleChatClick(e) {
        e.preventDefault();
        e.stopPropagation();
        const el = e.currentTarget;
        if (selectedChats.has(el)) {
            selectedChats.delete(el);
            el.classList.remove('chat-selected');
        } else {
            selectedChats.add(el);
            el.classList.add('chat-selected');
        }
        updateDelBtn();
    }

    function updateDelBtn(text) {
        const btn = document.getElementById('del-btn');
        if (btn) {
            btn.innerHTML = text ? text : `🔥 Delete (${selectedChats.size})`;
            btn.disabled = selectedChats.size === 0;
        }
    }

    const selectAllChats = () => {
        document.querySelectorAll('a[href^="/c/"]').forEach(chat => {
            if (chat.style.display !== 'none') {
                selectedChats.add(chat);
                chat.classList.add('chat-selected');
            }
        });
        updateDelBtn();
    };

    const deselectAllChats = () => {
        selectedChats.forEach(c => c.classList.remove('chat-selected'));
        selectedChats.clear();
        updateDelBtn();
    };

    // --- Core: Turbo Delete ---
    async function deleteSelectedChats() {
        if (selectedChats.size === 0) return;
        // Native confirmation only for large batches to prevent accidents
        if (selectedChats.size > 10 && !confirm(`Permanently delete ${selectedChats.size} chats?`)) return;

        const token = await getAuthToken();
        if (!token) return;

        const chats = Array.from(selectedChats);
        const delBtn = document.getElementById('del-btn');
        const toggleBtn = document.getElementById('toggle-select-btn');

        delBtn.disabled = true;
        toggleBtn.disabled = true;

        let done = 0;
        const total = chats.length;
        const queue = [...chats];
        const active = new Set();
        const MAX_ASYNC = 5;

        const updateUI = () => updateDelBtn(`🔥 ${done}/${total}`);

        async function runner() {
            while (queue.length > 0 || active.size > 0) {
                while (active.size < MAX_ASYNC && queue.length > 0) {
                    const el = queue.shift();
                    const id = el.getAttribute('href').split('/').pop().split('?')[0];

                    const p = fetch(`https://chatgpt.com/backend-api/conversation/${id}`, {
                        method: "PATCH",
                        headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
                        body: JSON.stringify({ is_visible: false })
                    })
                    .then(res => {
                        if (!res.ok) throw new Error();
                        done++;
                        el.classList.add('burning');
                        setTimeout(() => { el.style.display = 'none'; el.remove(); }, 500);
                    })
                    .catch(() => {})
                    .finally(() => {
                        active.delete(p);
                        updateUI();
                    });
                    active.add(p);
                }
                if (active.size > 0) await Promise.race(active);
                else break;
            }
        }

        await runner();

        toggleBtn.disabled = false;
        toggleSelectionMode(); // Exit clean
    }

    // --- Observer ---
    const observer = new MutationObserver(() => {
        if (document.querySelector('#sidebar-header') && !document.getElementById('toggle-select-btn')) {
            initialize();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

})();