Claude Project Bulk Delete (v7)

Add checkboxes to select multiple conversations in Claude Projects for bulk deletion - Drag the button to move, click to toggle

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

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Claude Project Bulk Delete (v7)
// @namespace    http://tampermonkey.net/
// @version      7
// @description  Add checkboxes to select multiple conversations in Claude Projects for bulk deletion - Drag the button to move, click to toggle
// @author       Solomon
// @match        https://claude.ai/project/*
// @icon         https://claude.ai/favicon.ico
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    console.log('🗑️ Claude Project Bulk Delete v7 loading...');

    // ═══════════════════════════════════════════════════════════════════════════
    // 🎨 STYLES
    // ═══════════════════════════════════════════════════════════════════════════

    GM_addStyle(`
        /* ═══════════════════════════════════════════════════════════════════════
           FAB Container
           ═══════════════════════════════════════════════════════════════════════ */
        #cpbd-fab {
            position: fixed;
            bottom: 20px;
            right: 20px;
            z-index: 999999;
            display: flex;
            flex-direction: column;
            gap: 6px;
            align-items: flex-end;
            touch-action: none;
        }

        /* Main Toggle Button - v7: DRAGGABLE */
        #cpbd-toggle-btn {
            width: 42px;
            height: 42px;
            border-radius: 50%;
            background: #dc2626 !important;
            border: 2px solid white !important;
            color: white !important;
            font-size: 18px;
            cursor: grab;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.25);
            transition: transform 0.2s ease, background 0.2s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            position: relative;
            user-select: none;
        }

        #cpbd-toggle-btn:hover {
            transform: scale(1.05);
        }

        #cpbd-toggle-btn.active {
            background: #16a34a !important;
        }

        #cpbd-toggle-btn.dragging {
            cursor: grabbing;
            transform: scale(1.1);
            box-shadow: 0 5px 20px rgba(0, 0, 0, 0.35);
        }

        /* Counter Badge */
        #cpbd-counter {
            position: absolute;
            top: -4px;
            right: -4px;
            background: #fbbf24;
            color: #000;
            font-size: 10px;
            font-weight: bold;
            min-width: 18px;
            height: 18px;
            border-radius: 9px;
            display: none;
            align-items: center;
            justify-content: center;
            padding: 0 4px;
            pointer-events: none;
        }

        #cpbd-counter.visible {
            display: flex;
        }

        /* Action Buttons Container */
        #cpbd-actions {
            display: none;
            flex-direction: column;
            gap: 5px;
            align-items: flex-end;
        }

        #cpbd-actions.visible {
            display: flex;
        }

        /* Action Buttons */
        .cpbd-action-btn {
            padding: 6px 10px;
            border-radius: 14px;
            border: none;
            font-size: 11px;
            font-weight: 600;
            cursor: pointer;
            display: flex;
            align-items: center;
            gap: 4px;
            white-space: nowrap;
            box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
            transition: all 0.15s ease;
        }

        #cpbd-delete-btn {
            background: #dc2626 !important;
            color: white !important;
        }

        #cpbd-delete-btn:disabled {
            background: #9ca3af !important;
            cursor: not-allowed;
        }

        #cpbd-select-all-btn {
            background: #2563eb !important;
            color: white !important;
        }

        #cpbd-clear-btn {
            background: #6b7280 !important;
            color: white !important;
        }

        /* ═══════════════════════════════════════════════════════════════════════
           CHECKBOX STYLING
           ═══════════════════════════════════════════════════════════════════════ */

        .cpbd-cb-wrapper {
            position: absolute !important;
            left: -28px !important;
            top: 50% !important;
            transform: translateY(-50%) !important;
            z-index: 1000 !important;
            width: 20px !important;
            height: 20px !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            opacity: 0;
            transition: opacity 0.2s ease;
            pointer-events: none;
        }

        body.cpbd-active .cpbd-cb-wrapper {
            opacity: 1 !important;
            pointer-events: auto !important;
        }

        .cpbd-cb {
            width: 16px !important;
            height: 16px !important;
            cursor: pointer !important;
            accent-color: #dc2626 !important;
            margin: 0 !important;
            padding: 0 !important;
        }

        .cpbd-conv-row {
            position: relative !important;
            margin-left: 0 !important;
            transition: all 0.2s ease;
        }

        body.cpbd-active .cpbd-conv-row {
            margin-left: 28px !important;
        }

        .cpbd-conv-row.cpbd-sel {
            background: rgba(220, 38, 38, 0.1) !important;
            border-radius: 6px;
        }

        /* ═══════════════════════════════════════════════════════════════════════
           MODAL
           ═══════════════════════════════════════════════════════════════════════ */

        #cpbd-overlay {
            position: fixed;
            inset: 0;
            background: rgba(0,0,0,0.5);
            z-index: 9999999;
            display: none;
            align-items: center;
            justify-content: center;
        }

        #cpbd-overlay.show {
            display: flex;
        }

        #cpbd-modal {
            background: white;
            border-radius: 10px;
            padding: 18px;
            max-width: 320px;
            width: 90%;
            box-shadow: 0 15px 40px rgba(0,0,0,0.3);
        }

        #cpbd-modal h3 {
            margin: 0 0 10px 0;
            color: #dc2626;
            font-size: 15px;
        }

        #cpbd-modal p {
            margin: 0 0 16px 0;
            color: #555;
            font-size: 13px;
            line-height: 1.4;
        }

        #cpbd-modal-btns {
            display: flex;
            gap: 8px;
            justify-content: flex-end;
        }

        #cpbd-modal-btns button {
            padding: 8px 14px;
            border-radius: 5px;
            border: none;
            font-size: 12px;
            font-weight: 500;
            cursor: pointer;
        }

        #cpbd-btn-cancel {
            background: #e5e7eb;
            color: #333;
        }

        #cpbd-btn-confirm {
            background: #dc2626;
            color: white;
        }

        /* Progress */
        #cpbd-progress {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 18px 24px;
            border-radius: 10px;
            box-shadow: 0 12px 35px rgba(0,0,0,0.25);
            z-index: 99999999;
            display: none;
            text-align: center;
            min-width: 200px;
        }

        #cpbd-progress.show {
            display: block;
        }

        #cpbd-progress-text {
            margin-bottom: 10px;
            font-size: 12px;
            color: #333;
        }

        #cpbd-progress-bar {
            height: 5px;
            background: #e5e7eb;
            border-radius: 3px;
            overflow: hidden;
        }

        #cpbd-progress-fill {
            height: 100%;
            background: #dc2626;
            width: 0%;
            transition: width 0.2s;
        }

        /* Dark mode */
        [data-mode="dark"] #cpbd-modal,
        [data-mode="dark"] #cpbd-progress {
            background: #1f2937;
            color: #e5e7eb;
        }
        [data-mode="dark"] #cpbd-modal p {
            color: #9ca3af;
        }
        [data-mode="dark"] #cpbd-btn-cancel {
            background: #374151;
            color: #e5e7eb;
        }
    `);

    // ═══════════════════════════════════════════════════════════════════════════
    // 📊 STATE
    // ═══════════════════════════════════════════════════════════════════════════

    const state = {
        active: false,
        selected: new Map()
    };

    // ═══════════════════════════════════════════════════════════════════════════
    // 🔧 HELPERS
    // ═══════════════════════════════════════════════════════════════════════════

    function getOrgId() {
        const m = document.cookie.match(/lastActiveOrg=([^;]+)/);
        return m ? m[1] : null;
    }

    async function deleteConv(id) {
        const org = getOrgId();
        if (!org) return false;
        try {
            const r = await fetch(`/api/organizations/${org}/chat_conversations/${id}`, {
                method: 'DELETE',
                credentials: 'include'
            });
            return r.ok;
        } catch (e) {
            return false;
        }
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // 🔍 FIND CONVERSATIONS
    // ═══════════════════════════════════════════════════════════════════════════

    function findConversations() {
        const results = [];
        const links = document.querySelectorAll('a[href^="/chat/"]');

        links.forEach(link => {
            const href = link.getAttribute('href');
            const match = href.match(/^\/chat\/([a-f0-9-]+)/);
            if (!match) return;

            const id = match[1];
            const title = link.textContent?.trim() || 'Untitled';

            let row = link.parentElement;
            for (let i = 0; i < 6 && row; i++) {
                if (row.textContent?.includes('Last message') &&
                    row.textContent?.includes('ago')) {
                    break;
                }
                row = row.parentElement;
            }

            if (row && row !== document.body && !row.matches('main, [class*="flex-col"]')) {
                if (!results.some(r => r.id === id)) {
                    results.push({ id, title, row, link });
                }
            }
        });

        console.log(`🔍 Found ${results.length} conversations`);
        return results;
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // 🎯 UI CREATION
    // ═══════════════════════════════════════════════════════════════════════════

    function createUI() {
        // FAB
        const fab = document.createElement('div');
        fab.id = 'cpbd-fab';
        fab.innerHTML = `
            <div id="cpbd-actions">
                <button id="cpbd-delete-btn" class="cpbd-action-btn" disabled>🗑️ (0)</button>
                <button id="cpbd-select-all-btn" class="cpbd-action-btn">☑️ All</button>
                <button id="cpbd-clear-btn" class="cpbd-action-btn">✖️</button>
            </div>
            <button id="cpbd-toggle-btn">🗑️<span id="cpbd-counter">0</span></button>
        `;
        document.body.appendChild(fab);

        // Modal
        const overlay = document.createElement('div');
        overlay.id = 'cpbd-overlay';
        overlay.innerHTML = `
            <div id="cpbd-modal">
                <h3>⚠️ Confirm Delete</h3>
                <p>Delete <strong id="cpbd-del-count">0</strong> conversation(s)?<br>
                <span style="color:#dc2626;font-size:12px">This cannot be undone!</span></p>
                <div id="cpbd-modal-btns">
                    <button id="cpbd-btn-cancel">Cancel</button>
                    <button id="cpbd-btn-confirm">🗑️ Delete</button>
                </div>
            </div>
        `;
        document.body.appendChild(overlay);

        // Progress
        const prog = document.createElement('div');
        prog.id = 'cpbd-progress';
        prog.innerHTML = `
            <div id="cpbd-progress-text">Deleting...</div>
            <div id="cpbd-progress-bar"><div id="cpbd-progress-fill"></div></div>
        `;
        document.body.appendChild(prog);

        // Action button events
        document.getElementById('cpbd-delete-btn').onclick = showModal;
        document.getElementById('cpbd-select-all-btn').onclick = selectAll;
        document.getElementById('cpbd-clear-btn').onclick = clearSel;
        document.getElementById('cpbd-btn-cancel').onclick = hideModal;
        document.getElementById('cpbd-btn-confirm').onclick = doDelete;
        overlay.onclick = e => { if (e.target === overlay) hideModal(); };

        // ═══════════════════════════════════════════════════════════════════════
        // v7: DRAG + CLICK on same button
        // Click = toggle mode, Drag = move FAB
        // ═══════════════════════════════════════════════════════════════════════

        const toggleBtn = document.getElementById('cpbd-toggle-btn');
        let isDragging = false;
        let hasMoved = false;
        let startX = 0;
        let startY = 0;
        let offsetX = 0;
        let offsetY = 0;
        const DRAG_THRESHOLD = 5; // pixels before considered a drag

        toggleBtn.addEventListener('mousedown', startDrag);
        toggleBtn.addEventListener('touchstart', startDrag, { passive: false });

        function startDrag(e) {
            e.preventDefault();

            const clientX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
            const clientY = e.type === 'touchstart' ? e.touches[0].clientY : e.clientY;

            startX = clientX;
            startY = clientY;
            hasMoved = false;
            isDragging = true;

            const rect = fab.getBoundingClientRect();
            offsetX = clientX - rect.left;
            offsetY = clientY - rect.top;

            document.addEventListener('mousemove', onDrag);
            document.addEventListener('mouseup', stopDrag);
            document.addEventListener('touchmove', onDrag, { passive: false });
            document.addEventListener('touchend', stopDrag);
        }

        function onDrag(e) {
            if (!isDragging) return;

            const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
            const clientY = e.type === 'touchmove' ? e.touches[0].clientY : e.clientY;

            // Check if moved past threshold
            const dx = Math.abs(clientX - startX);
            const dy = Math.abs(clientY - startY);

            if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {
                hasMoved = true;
                toggleBtn.classList.add('dragging');
            }

            if (hasMoved) {
                e.preventDefault();

                let newX = clientX - offsetX;
                let newY = clientY - offsetY;

                // Keep within viewport bounds
                const maxX = window.innerWidth - fab.offsetWidth - 5;
                const maxY = window.innerHeight - fab.offsetHeight - 5;

                newX = Math.max(5, Math.min(newX, maxX));
                newY = Math.max(5, Math.min(newY, maxY));

                fab.style.left = newX + 'px';
                fab.style.top = newY + 'px';
                fab.style.right = 'auto';
                fab.style.bottom = 'auto';
            }
        }

        function stopDrag(e) {
            isDragging = false;
            toggleBtn.classList.remove('dragging');

            document.removeEventListener('mousemove', onDrag);
            document.removeEventListener('mouseup', stopDrag);
            document.removeEventListener('touchmove', onDrag);
            document.removeEventListener('touchend', stopDrag);

            // If didn't move much, treat as click
            if (!hasMoved) {
                toggle();
            }
        }
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // 🔄 TOGGLE & CHECKBOX INJECTION
    // ═══════════════════════════════════════════════════════════════════════════

    function toggle() {
        state.active = !state.active;
        document.getElementById('cpbd-toggle-btn').classList.toggle('active', state.active);
        document.getElementById('cpbd-actions').classList.toggle('visible', state.active);
        document.body.classList.toggle('cpbd-active', state.active);

        if (state.active) {
            addCheckboxes();
        } else {
            clearSel();
            removeCheckboxes();
        }
    }

    function addCheckboxes() {
        const convs = findConversations();

        convs.forEach(conv => {
            if (conv.row.classList.contains('cpbd-conv-row')) return;

            conv.row.classList.add('cpbd-conv-row');

            const wrapper = document.createElement('div');
            wrapper.className = 'cpbd-cb-wrapper';

            const cb = document.createElement('input');
            cb.type = 'checkbox';
            cb.className = 'cpbd-cb';
            cb.dataset.convId = conv.id;
            cb.dataset.convTitle = conv.title;

            cb.onclick = e => e.stopPropagation();

            cb.onchange = () => {
                if (cb.checked) {
                    state.selected.set(conv.id, { title: conv.title, row: conv.row });
                    conv.row.classList.add('cpbd-sel');
                } else {
                    state.selected.delete(conv.id);
                    conv.row.classList.remove('cpbd-sel');
                }
                updateCount();
            };

            wrapper.appendChild(cb);
            conv.row.insertBefore(wrapper, conv.row.firstChild);
        });
    }

    function removeCheckboxes() {
        document.querySelectorAll('.cpbd-cb-wrapper').forEach(el => el.remove());
        document.querySelectorAll('.cpbd-conv-row').forEach(el => {
            el.classList.remove('cpbd-conv-row', 'cpbd-sel');
        });
    }

    function updateCount() {
        const n = state.selected.size;
        const counter = document.getElementById('cpbd-counter');
        counter.textContent = n;
        counter.classList.toggle('visible', n > 0);

        const btn = document.getElementById('cpbd-delete-btn');
        btn.textContent = `🗑️ (${n})`;
        btn.disabled = n === 0;
    }

    function selectAll() {
        document.querySelectorAll('.cpbd-cb').forEach(cb => {
            if (!cb.checked) {
                cb.checked = true;
                cb.onchange();
            }
        });
    }

    function clearSel() {
        document.querySelectorAll('.cpbd-cb').forEach(cb => {
            if (cb.checked) {
                cb.checked = false;
                cb.onchange();
            }
        });
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // 🗑️ DELETE FUNCTIONS
    // ═══════════════════════════════════════════════════════════════════════════

    function showModal() {
        if (state.selected.size === 0) return;
        document.getElementById('cpbd-del-count').textContent = state.selected.size;
        document.getElementById('cpbd-overlay').classList.add('show');
    }

    function hideModal() {
        document.getElementById('cpbd-overlay').classList.remove('show');
    }

    async function doDelete() {
        hideModal();

        const items = Array.from(state.selected.entries());
        const total = items.length;
        let done = 0;

        const prog = document.getElementById('cpbd-progress');
        const txt = document.getElementById('cpbd-progress-text');
        const fill = document.getElementById('cpbd-progress-fill');

        prog.classList.add('show');

        for (const [id, data] of items) {
            txt.textContent = `${done + 1}/${total}`;
            fill.style.width = `${((done + 1) / total) * 100}%`;

            const ok = await deleteConv(id);
            if (ok) {
                done++;
                state.selected.delete(id);

                if (data.row) {
                    data.row.style.transition = 'all 0.25s ease';
                    data.row.style.opacity = '0';
                    data.row.style.transform = 'translateX(30px)';
                    setTimeout(() => data.row.remove(), 250);
                }
            }

            await new Promise(r => setTimeout(r, 350));
        }

        prog.classList.remove('show');
        fill.style.width = '0%';
        updateCount();

        console.log(`✅ Deleted ${done}/${total} conversations`);
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // 👁️ MUTATION OBSERVER
    // ═══════════════════════════════════════════════════════════════════════════

    function observe() {
        new MutationObserver(() => {
            if (state.active) {
                addCheckboxes();
            }
        }).observe(document.body, { childList: true, subtree: true });
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // 🚀 INIT
    // ═══════════════════════════════════════════════════════════════════════════

    function init() {
        if (!location.pathname.startsWith('/project/')) {
            return;
        }

        console.log('🗑️ v7 init...');
        createUI();
        observe();
        console.log('✅ Ready!');
    }

    setTimeout(init, 1500);

})();