Claude - Bulk Delete Conversations

Add checkboxes to chat list for bulk delete conversations

// ==UserScript==
// @name         Claude - Bulk Delete Conversations
// @description  Add checkboxes to chat list for bulk delete conversations
// @author       intenzemotion
// @namespace    https://greasyfork.org/en/users/923450
// @version      1.2
// @license      MIT
// @match        https://claude.ai/*
// @run-at       document-end
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  let org = '';
  let isBulkMode = false;
  let buttonObserver = null;
  let selectAllCheckbox = null;

  // Retrieve a cookie value by name
  const getCookieValue = (name) => document.cookie.split(`; ${name}=`).pop().split(';').shift();

  // Create an HTML element with specified styles and content
  const createElement = (tagName, styles, content = '') => {
    const element = document.createElement(tagName);
    element.innerHTML = content.trim();
    Object.assign(element.style, styles);
    return element;
  };

  // Set styles for "Bulk Mode" button based on its state
  const setButtonStyles = (button, state) => {
    const baseStyles = {
      position: 'fixed',
      bottom: '20px',
      right: '20px',
      zIndex: '10',
      transition: 'filter 0.3s ease',
      padding: '10px 20px',
      color: 'white',
      border: 'none',
      borderRadius: '5px',
      cursor: 'pointer',
    };

    const stateStyles = {
      default: { backgroundColor: '#2D5F91', text: 'Bulk Mode' },
      cancel: { backgroundColor: '#6E788C', text: 'Cancel' },
      delete: { backgroundColor: '#BE4646', text: 'Delete Selected' },
    };

    Object.assign(button.style, baseStyles, { backgroundColor: stateStyles[state].backgroundColor });
    button.textContent = stateStyles[state].text;
    button.onmouseover = () => (button.style.filter = 'brightness(1.1)');
    button.onmouseout = () => (button.style.filter = 'none');
  };

  // Show a loading message during deletion process
  const showLoading = (message) => {
    const loadingDiv = createElement('div', {
      position: 'fixed',
      top: '50%',
      left: '50%',
      transform: 'translate(-50%, -50%)',
      backgroundColor: 'rgba(0, 0, 0, 0.7)',
      color: 'white',
      padding: '20px',
      borderRadius: '5px',
      zIndex: '10',
    }, `<div id="loading-message">${message}</div>`);
    document.body.appendChild(loadingDiv);
  };

  // Delete selected conversations
  const deleteSelectedChats = async () => {
    if (!org) {
      alert('Could not retrieve current organization id');
      return;
    }

    const checkedItems = document.querySelectorAll('ul.flex.flex-col.gap-3 > li input[type="checkbox"]:checked');
    const uuids = Array.from(checkedItems).map((checkbox) => checkbox.closest('li').querySelector('a').getAttribute('href').split('/').pop());

    if (!uuids.length) {
      alert('No conversations selected for deletion.');
      return;
    }

    if (confirm(`Delete ${uuids.length} conversation(s)?`)) {
      showLoading('Deleting conversations...');
      try {
        await Promise.all(uuids.map((uuid) =>
          fetch(`https://claude.ai/api/organizations/${org}/chat_conversations/${uuid}`, {
            method: 'DELETE',
            credentials: 'include',
          })
        ));
        window.location.reload();
      } catch (error) {
        console.error('Error deleting conversations:', error);
        document.getElementById('loading-message')?.remove();
        alert('An error occurred while deleting conversations. Please try again.');
      }
    }
  };

  // Toggle checkboxes for each conversation
  const toggleCheckboxes = (add) => {
    const listItems = document.querySelectorAll('ul.flex.flex-col.gap-3 > li');
    listItems.forEach((item, index) => {
      const existingCheckbox = item.querySelector('input[type="checkbox"]');
      if (add && !existingCheckbox) {
        const checkbox = createElement('input', {
          position: 'absolute',
          right: '10px',
          top: '50%',
          transform: 'translateY(-50%)',
          zIndex: '10',
        });
        checkbox.type = 'checkbox';
        checkbox.id = `conversation-checkbox-${index}`;
        item.style.position = 'relative';
        item.appendChild(checkbox);
        checkbox.addEventListener('change', updateBulkModeState);
      } else if (!add && existingCheckbox) {
        existingCheckbox.remove();
      }
    });
    toggleSelectAllCheckbox(add);
  };

  // Toggle "Select All" checkbox
  const toggleSelectAllCheckbox = (add) => {
    const container = document.querySelector('ul.flex.flex-col.gap-3');
    if (!container) return;

    if (add && !selectAllCheckbox) {
      const selectAllLabel = createElement('label', {
        display: 'flex',
        alignItems: 'center',
        marginBottom: '10px',
        cursor: 'pointer',
      }, `<input type="checkbox" id="select-all-checkbox" style="margin-right: 5px;">Select All`);

      selectAllCheckbox = selectAllLabel.querySelector('#select-all-checkbox');
      selectAllCheckbox.addEventListener('change', handleSelectAll);
      container.parentNode.insertBefore(selectAllLabel, container);
    } else if (!add && selectAllCheckbox) {
      selectAllCheckbox.closest('label').remove();
      selectAllCheckbox = null;
    }
  };

  // Handle "Select All" checkbox changes
  const handleSelectAll = () => {
    const checkboxes = document.querySelectorAll('ul.flex.flex-col.gap-3 > li input[type="checkbox"]');
    checkboxes.forEach((checkbox) => (checkbox.checked = selectAllCheckbox.checked));
    updateBulkModeState();
  };

  // Update "Bulk Mode" button and "Select All" checkbox states
  const updateBulkModeState = () => {
    const bulkButton = document.getElementById('bulkDeleteButton');
    if (!bulkButton) return;
    const checkboxes = document.querySelectorAll('ul.flex.flex-col.gap-3 > li input[type="checkbox"]');
    const checkedItems = Array.from(checkboxes).filter(cb => cb.checked);
    setButtonStyles(bulkButton, checkedItems.length ? 'delete' : 'cancel');

    if (selectAllCheckbox) {
      selectAllCheckbox.checked = checkboxes.length && checkedItems.length === checkboxes.length;
      selectAllCheckbox.indeterminate = checkedItems.length > 0 && checkedItems.length < checkboxes.length;
    }
  };

  // Toggle "Bulk Mode" on and off
  const toggleBulkMode = () => {
    isBulkMode = !isBulkMode;
    toggleCheckboxes(isBulkMode);
    const bulkButton = document.getElementById('bulkDeleteButton');
    if (bulkButton) setButtonStyles(bulkButton, isBulkMode ? 'cancel' : 'default');
    if (isBulkMode) updateBulkModeState();
  };

  // Handle clicks on "Bulk Mode" button
  const handleBulkButtonClick = () => {
    if (!isBulkMode) {
      toggleBulkMode();
    } else {
      const checkedItems = document.querySelectorAll('ul.flex.flex-col.gap-3 > li input[type="checkbox"]:checked');
      checkedItems.length ? deleteSelectedChats() : toggleBulkMode();
    }
  };

  // Update visibility of "Bulk Mode" button
  const toggleBulkButton = (show) => {
    const existingButton = document.getElementById('bulkDeleteButton');
    if (show && !existingButton) {
      const button = createElement('button', {
        padding: '10px 20px',
        color: 'white',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
      }, 'Bulk Mode');
      button.id = 'bulkDeleteButton';
      button.addEventListener('click', handleBulkButtonClick);
      setButtonStyles(button, 'default');
      document.body.appendChild(button);
    } else if (!show && existingButton) {
      existingButton.remove();
      if (isBulkMode) toggleBulkMode();
    }
  };

  // Handle navigation changes and update UI accordingly
  const handleNavigation = () => {
    const isRecentsPage = window.location.pathname === '/recents';
    toggleBulkButton(isRecentsPage);

    if (isRecentsPage) {
      if (!buttonObserver) {
        buttonObserver = new MutationObserver((mutations) => {
          if (mutations.some((mutation) => mutation.type === 'childList')) {
            toggleBulkButton(true);
          }
        });
        buttonObserver.observe(document.body, { childList: true, subtree: true });
      }
    } else if (buttonObserver) {
      buttonObserver.disconnect();
      buttonObserver = null;
    }
  };

  // Initialize script
  const init = () => {
    org = getCookieValue('lastActiveOrg') || '';
    handleNavigation();
    window.addEventListener('popstate', handleNavigation);
    const pushState = history.pushState;
    history.pushState = function () {
      pushState.apply(history, arguments);
      handleNavigation();
    };
  };

  // Start the script
  init();
})();