M365 Copilot Bulk Delete

Bulk delete chats in M365 Copilot

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(function() {
    'use strict';

    const SELECTORS = {
        // Universal Filter: Selects the row ONLY IF it contains the chat menu item button
        chatRow: 'div.fai-CopilotNavSubItemGroup div.fui-SplitNavItem:has(button[aria-roledescription="Menu item"])',
        // The main button containing the ID
        chatBtn: 'button[aria-roledescription="Menu item"]',
        // The "More" button (sibling of chatBtn)
        moreBtn: '.fai-SplitCopilotNavItem__menuButton',
        // Language-proof: Targets the menu item immediately following the visual divider
        dropdownDelete: 'div[role="separator"] + div[role="menuitem"]',
        // Language-proof: Targets the final confirm button that lacks the restorer attribute
        modalConfirm: '.fui-DialogActions button:not([data-tabster])',
        // Target for the "Select All" UI
        selectAllTarget: 'div.___13wxke1.f1xg1ack.f15twtuk.f19g0ac.fxugw4r',


    };
    const CONFIG = {
        // Active chat detection: attribute
        activeAttribute: 'aria-current', // You can set this to an attribute or a CSS class
        // Active chat detection: attribute value
        activeValue: 'page', // Set to null if you only want to check for the presence of an attribute
        // Active chat detection: class
        activeClass: null    // Or set to a class like 'is-active' if the site uses classes
    };

    const DELAY = 300;
    let selectedIds = new Set();
    let isDeleting = false;

    const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

    const waitForElement = (selector, timeout = 5000) => {
        return new Promise((resolve) => {
            const start = Date.now();
            const interval = setInterval(() => {
                const el = document.querySelector(selector);
                if (el || (Date.now() - start) > timeout) {
                    clearInterval(interval);
                    resolve(el);
                }
            }, 100);
        });
    };

    // --- Styles ---
    const style = document.createElement('style');
    style.innerHTML = `
        .bulk-delete-checkbox { margin: 0 8px; cursor: pointer; z-index: 10; flex-shrink: 0; transform: scale(1.1); }
        #bulk-delete-bar {
            position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
            background: #202124; color: white; padding: 12px 24px; border-radius: 32px;
            display: none; align-items: center; gap: 16px; box-shadow: 0 8px 24px rgba(0,0,0,0.4);
            z-index: 10000; border: 1px solid #3c4043; transition: all 0.3s ease;
        }
        #bulk-delete-bar.finished { background: #1e8e3e; border-color: #1e8e3e; }
        #bulk-delete-bar button {
            background: #f28b82; color: #202124; border: none; padding: 8px 18px;
            border-radius: 20px; cursor: pointer; font-weight: bold;
        }
        #bulk-delete-bar button:hover { background: #ee675c; }
        ${SELECTORS.chatRow} { display: flex !important; align-items: center !important; }
    `;
    document.head.appendChild(style);

    const actionBar = document.createElement('div');
    actionBar.id = 'bulk-delete-bar';
    actionBar.innerHTML = `
        <span id="selected-count">0 selected</span>
        <button id="execute-bulk-delete">Confirm Bulk Delete 🗑️</button>
    `;
    document.body.appendChild(actionBar);

    function updateActionBar(message = null) {
        const countSpan = document.getElementById('selected-count');
        const deleteBtn = document.getElementById('execute-bulk-delete');
        if (message) {
            countSpan.innerText = message;
            deleteBtn.style.display = 'none';
            actionBar.classList.add('finished');
            actionBar.style.display = 'flex';
        } else {
            const count = selectedIds.size;
            countSpan.innerText = `${count} selected`;
            deleteBtn.style.display = 'inline-block';
            actionBar.classList.remove('finished');
            actionBar.style.display = count > 0 ? 'flex' : 'none';
        }
    }

    function injectCheckbox(row) {
        if (row.querySelector('.bulk-delete-checkbox')) return;

        const btn = row.querySelector(SELECTORS.chatBtn);
        if (!btn || !btn.id) return;

        const threadId = btn.id;
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.className = 'bulk-delete-checkbox';

        if (selectedIds.has(threadId)) checkbox.checked = true;

        checkbox.addEventListener('change', (e) => {
            if (e.target.checked) selectedIds.add(threadId);
            else selectedIds.delete(threadId);
            updateActionBar();
        });

        checkbox.addEventListener('click', (e) => e.stopPropagation());
        row.prepend(checkbox);
    }

    async function injectSelectAll() {
        if (isDeleting) return;
        const header = document.querySelector(SELECTORS.selectAllTarget);
        if (!header || header.querySelector('#select-all-wrapper')) return;

        const wrapper = document.createElement('div');
        wrapper.id = 'select-all-wrapper';
        wrapper.style.cssText = 'display:flex; align-items:center; padding: 8px 16px;';
        wrapper.innerHTML = `
            <input type="checkbox" id="select-all-chats" class="bulk-delete-checkbox">
            <label for="select-all-chats" style="cursor:pointer; font-size: 13px; color: #9aa0a6;">Select All</label>
        `;

        wrapper.querySelector('#select-all-chats').addEventListener('change', (e) => {
            const isChecked = e.target.checked;
            document.querySelectorAll(SELECTORS.chatRow).forEach(row => {
                const btn = row.querySelector(SELECTORS.chatBtn);
                const cb = row.querySelector('.bulk-delete-checkbox');
                if (btn && cb) {
                    cb.checked = isChecked;
                    if (isChecked) selectedIds.add(btn.id);
                    else selectedIds.delete(btn.id);
                }
            });
            updateActionBar();
        });
        header.prepend(wrapper);
    }

    async function deleteSingleItem(threadId) {
        const btn = document.getElementById(threadId);
        if (!btn) return;

        try {
            const row = btn.closest(SELECTORS.chatRow);
            const moreBtn = row.querySelector(SELECTORS.moreBtn);
            if (!moreBtn) return;

            moreBtn.click();
            await sleep(DELAY + 300);

            const dropdownBtn = await waitForElement(SELECTORS.dropdownDelete);
            if (!dropdownBtn) return;
            dropdownBtn.click();
            await sleep(DELAY + 500);

            const confirmBtn = await waitForElement(SELECTORS.modalConfirm);
            if (confirmBtn) confirmBtn.click();

            await sleep(DELAY + 700);
        } catch (err) {
            console.error("Copilot Deletion error:", err);
        }
    }

    document.getElementById('execute-bulk-delete').addEventListener('click', async () => {
        const itemsToProcess = Array.from(selectedIds)
            .map(id => {
                const btn = document.getElementById(id);
                return { id: id, el: btn };
            })
            .filter(item => item.el !== null)
            .sort((a, b) => {
                const isActive = (el) => {
                    if (CONFIG.activeClass) return el.classList.contains(CONFIG.activeClass);
                    if (CONFIG.activeAttribute) return el.getAttribute(CONFIG.activeAttribute) === CONFIG.activeValue;
                    return false;
                };

                const aIsActive = isActive(a.el);
                const bIsActive = isActive(b.el);

                if (aIsActive && !bIsActive) return 1;
                if (!aIsActive && bIsActive) return -1;

                return b.el.getBoundingClientRect().top - a.el.getBoundingClientRect().top;
            });

        if (!itemsToProcess.length || !confirm(`Delete ${itemsToProcess.length} chats?`)) return;

        isDeleting = true;
        actionBar.style.display = 'none';

        for (const item of itemsToProcess) {
            await deleteSingleItem(item.id);
            selectedIds.delete(item.id);
        }

        isDeleting = false;
        selectedIds.clear();
        updateActionBar("Deletion Finished! ✅");

        setTimeout(() => {
            actionBar.style.display = 'none';
            updateActionBar();
        }, 3000);
    });

    const observer = new MutationObserver(() => {
        document.querySelectorAll(SELECTORS.chatRow).forEach(injectCheckbox);
        injectSelectAll();
    });

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