Copilot Bulk Delete

Bulk delete chats in Copilot

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Copilot Bulk Delete
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Bulk delete chats in Copilot
// @author       php
// @match        https://copilot.microsoft.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ==========================================
    // 1. 配置設定 (Configuration)
    // ==========================================
    const CONFIG = {
        delayMs: 200, // 自動化點擊的間隔時間
        selectors: {
            // 對話列最外層容器
            chatContainer: 'div[role="link"]',
            // 更多選項按鈕 (滑鼠移入後出現)
            moreButton: 'button[id^="conversation-options-"]',
            // 下拉選單中的「刪除」按鈕
            deleteMenuItem: "div[role='menu'] > div > button[role='menuitem']:last-child",
            // 彈出視窗中的「確認刪除」按鈕
            confirmDeleteBtn: `div[data-tabster*='"modalizer":{"id":"modal-'][data-tabster*='"isOthersAccessible":false,"isAlwaysAccessible":true,"isTrapped":true}']:nth-child(2) button:nth-child(1)`
        }
    };

    // ==========================================
    // 2. 建立懸浮的「批次刪除」控制面板 (外層容器)
    // ==========================================
    const batchDeleteContainer = document.createElement('div');
    batchDeleteContainer.id = 'batch-delete-island';
    // 樣式設定:黑色膠囊背景
    batchDeleteContainer.style.cssText = `
        position: fixed;
        bottom: 30px;
        left: 50%;
        transform: translateX(-50%);
        background-color: #202124;
        padding: 8px 12px 8px 24px;
        border-radius: 32px;
        display: none; /* 預設隱藏 */
        align-items: center;
        gap: 16px;
        box-shadow: 0 8px 24px rgba(0,0,0,0.3);
        z-index: 9999;
        transition: all 0.3s ease;
        font-family: sans-serif;
    `;

    // 建立顯示數量的文字
    const countDisplay = document.createElement('span');
    countDisplay.style.cssText = `
        color: white;
        font-size: 14px;
        font-weight: 500;
    `;

    // 建立真正的刪除按鈕 (紅色區域)
    const batchDeleteBtn = document.createElement('button');
    batchDeleteBtn.innerHTML = 'Confirm Bulk Delete 🗑️';
    batchDeleteBtn.style.cssText = `
        background-color: #f28b82;
        color: #202124;
        border: none;
        padding: 8px 20px;
        border-radius: 20px;
        cursor: pointer;
        font-weight: bold;
        font-size: 14px;
        transition: background-color 0.2s;
    `;
    batchDeleteBtn.onmouseover = () => batchDeleteBtn.style.backgroundColor = '#ee675c';
    batchDeleteBtn.onmouseout = () => batchDeleteBtn.style.backgroundColor = '#f28b82';

    batchDeleteContainer.appendChild(countDisplay);
    batchDeleteContainer.appendChild(batchDeleteBtn);
    document.body.appendChild(batchDeleteContainer);

    // ==========================================
    // 3. 核心邏輯:輔助函式
    // ==========================================

    // 延遲函式 (保留做為極短的緩衝使用)
    const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

    // 新增:事件驅動的 DOM 等待函式 (Event-Driven Wait)
    async function waitFor(predicate, timeout = 3000, context = 'condition') {
        const check = () => {
            try { return predicate(); } catch (e) { return false; }
        };

        const initial = check();
        if (initial) return initial;

        return new Promise((resolve) => {
            const observer = new MutationObserver(() => {
                const result = check();
                if (result) {
                    observer.disconnect();
                    resolve(result);
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true,
                attributes: true
            });

            setTimeout(() => {
                observer.disconnect();
                console.warn(`Timeout waiting for: ${context}`);
                resolve(null);
            }, timeout);
        });
    }

    // 模擬滑鼠移入 (Hover)
    const simulateHover = (element) => {
        const events = ['pointerover', 'mouseover', 'mouseenter'];
        events.forEach(type => {
            const event = new MouseEvent(type, { bubbles: true, cancelable: true, view: window });
            element.dispatchEvent(event);
        });
    };

    // 更新懸浮面板顯示狀態
    const updateButtonVisibility = () => {
        const checkedBoxes = document.querySelectorAll('.batch-delete-cb:checked');
        const count = checkedBoxes.length;

        // Reset "Select All" checkbox if no items are currently selected
        if (count === 0) {
            const selectAllCb = document.getElementById('select-all-checkbox');
            if (selectAllCb) selectAllCb.checked = false;
        }

        // 控制外層容器的顯示與隱藏
        batchDeleteContainer.style.display = count > 0 ? 'flex' : 'none';

        // 更新計數文字
        countDisplay.innerText = `${count} selected`;
    };

    // Tracks the last clicked checkbox for Shift + Click range selection
    let lastCheckedCb = null;

    // Selects or deselects a range of checkboxes between startCb and endCb
    const selectRange = (startCb, endCb, targetState) => {
        const allCbs = Array.from(document.querySelectorAll('.batch-delete-cb'));
        const startIdx = allCbs.indexOf(startCb);
        const endIdx = allCbs.indexOf(endCb);

        if (startIdx === -1 || endIdx === -1) return;

        const min = Math.min(startIdx, endIdx);
        const max = Math.max(startIdx, endIdx);

        for (let i = min; i <= max; i++) {
            allCbs[i].checked = targetState;
        }
        updateButtonVisibility();
    };

    // ==========================================
    // 4. 將「全選」按鈕注入到側邊欄容器內
    // ==========================================
    const injectSelectAllToSidebar = () => {
        const sidebarContent = document.querySelector('div.grow[data-testid="sidebar-expanded-content"]');

        if (sidebarContent && !document.getElementById('batch-select-all-wrapper')) {
            const wrapper = document.createElement('div');
            wrapper.id = 'batch-select-all-wrapper';
            // 樣式:與對話列保持一致的間距與對齊方式
            wrapper.style.cssText = `
                padding: 12px 16px;
                display: flex;
                align-items: center;
                cursor: pointer;
                user-select: none;
                border-bottom: 1px solid rgba(0,0,0,0.05);
            `;

            // 建立 Checkbox 元素
            const selectAllCheckbox = document.createElement('input');
            selectAllCheckbox.type = 'checkbox';
            selectAllCheckbox.id = 'select-all-checkbox';
            selectAllCheckbox.style.cssText = `
                width: 18px;
                height: 18px;
                cursor: pointer;
                margin-right: 12px;
                flex-shrink: 0;
            `;

            // 建立文字標籤
            const label = document.createElement('span');
            label.innerText = 'Select All';
            label.style.cssText = `
                font-size: 14px;
                color: #5f6368; /* 灰藍色調,符合圖片樣式 */
                font-family: inherit;
            `;

            // 點擊文字也能觸發 Checkbox
            wrapper.addEventListener('click', (e) => {
                // 如果點擊的是 wrapper 本身(非 checkbox),則手動切換狀態
                if (e.target !== selectAllCheckbox) {
                    selectAllCheckbox.checked = !selectAllCheckbox.checked;
                }

                // 執行全選邏輯
                const checkboxes = document.querySelectorAll('.batch-delete-cb');
                checkboxes.forEach(cb => {
                    cb.checked = selectAllCheckbox.checked;
                });

                // 更新主按鈕顯示狀態
                updateButtonVisibility();
            });

            wrapper.appendChild(selectAllCheckbox);
            wrapper.appendChild(label);

            // 將整個全選控制項插入到側邊欄的最上方
            sidebarContent.prepend(wrapper);
        }
    };

    // ==========================================
    // 5. 核心邏輯:UI 注入
    // ==========================================
    // 修改後的 UI 注入邏輯
    const injectCheckboxes = () => {
        const chatContainers = document.querySelectorAll(CONFIG.selectors.chatContainer);

        chatContainers.forEach(container => {
            // 避免重複加入
            if (container.querySelector('.batch-delete-cb')) return;

            // --- 修正佈局:確保容器內元素橫向排列並置中 ---
            container.style.display = 'flex';
            container.style.alignItems = 'center';
            container.style.flexDirection = 'row'; // 強制橫向
            // -------------------------------------------

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.className = 'batch-delete-cb';

            // 微調 Checkbox 樣式,增加間距並確保它不會縮小
            checkbox.style.cssText = `
                margin-right: 10px;
                margin-left: 5px;
                cursor: pointer;
                width: 16px;
                height: 16px;
                flex-shrink: 0; /* 確保 Checkbox 不會因為文字太長而被壓扁 */
                z-index: 10;
            `;

            // 避免點擊 Checkbox 時觸發進入對話的事件 (Supports Shift + Click range selection)
            checkbox.addEventListener('click', (e) => {
                e.stopPropagation();

                if (e.shiftKey && lastCheckedCb && document.body.contains(lastCheckedCb)) {
                    selectRange(lastCheckedCb, checkbox, checkbox.checked);
                } else {
                    updateButtonVisibility();
                }

                lastCheckedCb = checkbox;
            });

            // 將 Checkbox 插入到容器的最前方
            container.insertBefore(checkbox, container.firstChild);
        });
    };

    // 使用 MutationObserver 監聽 DOM 變化,確保動態載入的對話也能加上 Checkbox
    const observer = new MutationObserver(() => {
        injectCheckboxes();
        injectSelectAllToSidebar();
    });
    observer.observe(document.body, { childList: true, subtree: true });

    // ==========================================
    // 6. 核心邏輯:執行批次刪除
    // ==========================================
    batchDeleteBtn.addEventListener('click', async () => {
        const checkedBoxes = document.querySelectorAll('.batch-delete-cb:checked');

        // Confirm before starting deletion
        if (checkedBoxes.length === 0 || !confirm(`Delete ${checkedBoxes.length} chats?`)) return;

        // 避免使用者重複點擊
        batchDeleteBtn.disabled = true;
        batchDeleteBtn.style.backgroundColor = '#ccc';
        batchDeleteBtn.innerText = 'Deleting... Please wait';

        for (const checkbox of checkedBoxes) {
            try {
                // 找到 Checkbox 所在的對話容器
                const container = checkbox.closest(CONFIG.selectors.chatContainer);
                if (!container) continue;

                // 步驟 1: 模擬滑鼠移入對話容器,觸發「更多」按鈕顯示
                simulateHover(container);

                // 動態等待「更多」按鈕出現在 DOM 中,出現後立刻點擊
                const moreBtn = await waitFor(() => container.querySelector(CONFIG.selectors.moreButton), 2000, 'More Button');
                if (!moreBtn) {
                    console.warn('Cannot find the "More" button');
                    continue;
                }
                moreBtn.click();

                // 步驟 2: 動態等待下拉選單的「刪除」按鈕出現,出現後立刻點擊
                const deleteMenuBtn = await waitFor(() => document.querySelector(CONFIG.selectors.deleteMenuItem), 2000, 'Delete Menu Item');
                if (!deleteMenuBtn) {
                    console.warn('Cannot find "Delete" button in dropdown menu');
                    continue;
                }
                deleteMenuBtn.click();

                // 步驟 3: 動態等待彈出視窗的「確認刪除」按鈕出現,出現後立刻點擊
                const confirmBtn = await waitFor(() => document.querySelector(CONFIG.selectors.confirmDeleteBtn), 2000, 'Confirm Delete Button');
                if (!confirmBtn) {
                    console.warn('Cannot find "Confirm Delete" button in pop-up modal');
                    continue;
                }
                confirmBtn.click();

                // 步驟 4: 動態等待「確認刪除」按鈕從 DOM 消失 (代表彈出視窗已關閉)
                await waitFor(() => !document.querySelector(CONFIG.selectors.confirmDeleteBtn), 3000, 'Modal Dismissal');

                // 保留極短的緩衝,確保 React 狀態更新與網路請求不會與下一個迴圈衝突
                await sleep(50);

            } catch (error) {
                console.error('Deletion error:', error);
            }
        }

        // 執行完畢後恢復按鈕狀態並重新整理介面
        batchDeleteBtn.disabled = false;
        batchDeleteBtn.style.backgroundColor = '#d93025';
        updateButtonVisibility();
        console.log('Deletion Finished! ✅');
    });

})();