ChatGPT Bulk Deleter

Adds a "Select Chats" button to ChatGPT for deleting multiple conversations at once. Bypasses the UI and uses direct API calls for speed and reliability.

// ==UserScript==
// @name         ChatGPT Bulk Deleter
// @namespace    http://tampermonkey.net/
// @version      2.0.0
// @description  Adds a "Select Chats" button to ChatGPT for deleting multiple conversations at once. Bypasses the UI and uses direct API calls for speed and reliability.
// @author       @SavitarStorm @Tano
// @match        https://chatgpt.com/*
// @connect      chatgpt.com
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        .bulk-delete-controls {
            padding: 8px;
            display: flex;
            flex-direction: column;
            gap: 8px;
            width: 100%;
            border-bottom: 1px solid var(--token-border-light);
        }
        .bulk-delete-btn { display: inline-block; width: 100%; padding: 10px 12px; border: none; border-radius: 8px; cursor: pointer; text-align: center; font-size: 14px; font-weight: 500; transition: background-color 0.2s, color 0.2s; }
        #toggle-select-btn { background-color: #4C50D3; color: white; }
        #toggle-select-btn:hover { background-color: #3a3eab; }
        #toggle-select-btn.selection-active { background-color: #FFD6D6; color: #D34C4C; }
        #delete-selected-btn { background-color: #D34C4C; color: white; display: none; }
        #delete-selected-btn:hover { background-color: #b03a3a; }
        #delete-selected-btn:disabled { background-color: #7c7c7c; cursor: not-allowed; }
        .chat-selectable { cursor: cell !important; }
        a.chat-selected {
            background-color: rgba(76, 80, 211, 0.25) !important;
            outline: 2px solid #4C50D3 !important;
            border-radius: 8px;
        }
    `);

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

    async function getAuthToken() {
        if (authToken) return authToken;
        try {
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: "https://chatgpt.com/api/auth/session",
                    onload: resolve,
                    onerror: reject
                });
            });
            const data = JSON.parse(response.responseText);
            if (data && data.accessToken) {
                authToken = data.accessToken;
                return authToken;
            }
            throw new Error("accessToken not found in session response.");
        } catch (error) {
            console.error("Failed to retrieve auth token:", error);
            alert("Could not retrieve authorization token. The script cannot continue.");
            return null;
        }
    }

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

        getAuthToken();

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

        const toggleBtn = document.createElement('button');
        toggleBtn.id = 'toggle-select-btn';
        toggleBtn.className = 'bulk-delete-btn';
        toggleBtn.textContent = 'Select Chats to Delete';
        toggleBtn.onclick = toggleSelectionMode;

        const deleteBtn = document.createElement('button');
        deleteBtn.id = 'delete-selected-btn';
        deleteBtn.className = 'bulk-delete-btn';
        deleteBtn.textContent = 'Delete Selected (0)';
        deleteBtn.onclick = deleteSelectedChats;

        controlsContainer.appendChild(toggleBtn);
        controlsContainer.appendChild(deleteBtn);

        targetContainer.appendChild(controlsContainer);
    }

    function toggleSelectionMode() {
        selectionMode = !selectionMode;
        const toggleBtn = document.getElementById('toggle-select-btn');
        const deleteBtn = document.getElementById('delete-selected-btn');
        const chatItems = document.querySelectorAll('div#history a[href^="/c/"], div[role="presentation"] nav a[href^="/c/"]');

        if (selectionMode) {
            toggleBtn.textContent = 'Cancel Selection';
            toggleBtn.classList.add('selection-active');
            deleteBtn.style.display = 'block';
            chatItems.forEach(chat => {
                chat.classList.add('chat-selectable');
                chat.addEventListener('click', handleChatClick, true);
            });
        } else {
            toggleBtn.textContent = 'Select Chats to Delete';
            toggleBtn.classList.remove('selection-active');
            deleteBtn.style.display = 'none';
            chatItems.forEach(chat => {
                chat.classList.remove('chat-selectable', 'chat-selected');
                chat.removeEventListener('click', handleChatClick, true);
            });
            selectedChats.clear();
            updateDeleteButton();
        }
    }

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

    function updateDeleteButton() {
        const deleteBtn = document.getElementById('delete-selected-btn');
        if(deleteBtn) {
            deleteBtn.textContent = `Delete Selected (${selectedChats.size})`;
            deleteBtn.disabled = selectedChats.size === 0;
        }
    }

    async function deleteSelectedChats() {
        if (selectedChats.size === 0) return;
        const token = await getAuthToken();
        if (!token) return;
        if (!confirm(`Are you sure you want to delete ${selectedChats.size} chat(s)? This action is irreversible.`)) return;

        const chatsToDelete = Array.from(selectedChats);
        const total = chatsToDelete.length;
        const deleteBtn = document.getElementById('delete-selected-btn');
        const toggleBtn = document.getElementById('toggle-select-btn');
        deleteBtn.disabled = true;
        toggleBtn.disabled = true;
        deleteBtn.textContent = `Deleting ${total} chats... Please wait.`;

        const promises = chatsToDelete.map(chatElement => {
            const href = chatElement.getAttribute('href');
            const conversationId = href.split('/').pop();
            const chatTitle = chatElement.textContent.trim();

            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "PATCH",
                    url: `https://chatgpt.com/backend-api/conversation/${conversationId}`,
                    headers: {
                        "Content-Type": "application/json",
                        "Authorization": `Bearer ${token}`
                    },
                    data: JSON.stringify({ is_visible: false }),
                    onload: (response) => {
                        if (response.status >= 200 && response.status < 300) {
                            resolve({ chatElement });
                        } else {
                            reject({ chatElement, chatTitle, error: new Error(`Server responded with status ${response.status}`) });
                        }
                    },
                    onerror: (error) => reject({ chatElement, chatTitle, error })
                });
            });
        });

        const results = await Promise.allSettled(promises);

        let successCount = 0;
        let errorCount = 0;

        results.forEach(result => {
            if (result.status === 'fulfilled') {
                const { chatElement } = result.value;
                chatElement.style.transition = 'opacity 0.5s';
                chatElement.style.opacity = '0';
                setTimeout(() => chatElement.remove(), 500);
                successCount++;
            } else {
                const { chatElement, chatTitle, error } = result.reason;
                console.error(`  -> [FAIL] API call failed for "${chatTitle}":`, error);
                chatElement.style.outline = '2px solid red';
                errorCount++;
            }
        });

        alert(`Complete. Successfully deleted: ${successCount}. Errors: ${errorCount}.`);
        deleteBtn.disabled = false;
        toggleBtn.disabled = false;
        toggleSelectionMode();
    }

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

})();