Grok Easy Delete Chat

Delete Grok chat with only Cmd/Ctrl+Shift+Delete, auto-confirms popup.

Verzia zo dňa 07.06.2025. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         Grok Easy Delete Chat
// @namespace    nisc
// @version      2025.06.07-A
// @description  Delete Grok chat with only Cmd/Ctrl+Shift+Delete, auto-confirms popup.
// @homepageURL  https://raw.githubusercontent.com/nisc/grok-userscripts/
// @author       nisc
// @match        https://grok.com/*
// @icon         https://grok.com/favicon.ico
// @run-at       document-end
// @grant        none
// ==/UserScript==

(function() {
  'use strict';

  // Configuration object containing all adjustable settings and constants
  const CONFIG = {
    platform: {
      isMac: navigator.userAgent.includes('Mac')
    },
    shortcut: {
      key: navigator.userAgent.includes('Mac') ? 'Backspace' : 'Delete',
      modifier: navigator.userAgent.includes('Mac') ? 'metaKey' : 'ctrlKey',
      shift: true
    },
    // Timing delays (in ms) for various operations
    delays: {
      RETRY: 200,               // Delay between retries when finding chat element
      HOVER: 75,                // Delay after simulating hover events
      KEY_PRESS: 75,            // Delay between key press events
      MENU_OPEN: 125,           // Delay for menu to open after clicking history
      CLICK_TO_DELETE: 125,     // Delay between click and delete command
      DELETE_TO_ENTER: 125,     // Delay between delete and enter press
      FINAL_ESCAPE: 250,        // Final delay before pressing escape
    },
    // Key codes for special keys
    keyCodes: {
      Enter: {
        code: 'Enter',
        keyCode: 13
      },
      Escape: {
        code: 'Escape',
        keyCode: 27
      }
    },
    // DOM selectors used throughout the script
    selectors: {
      historyButton: 'button[aria-label="History"]',
      dialog: '[role="dialog"]',
      clickableArea: '.cursor-pointer',
      cmdkItem: '[cmdk-item]',
      queryBar: '.query-bar',
      privateClasses: ['bg-purple-50', 'dark:bg-purple-1000']
    },
    // Mouse event simulation coordinates
    mouse: {
      clientX: 100,
      clientY: 100
    },
    // Maximum retries for finding elements
    maxRetries: 5,
    // Text constants
    text: {
      current: 'Current'
    }
  };

  /**
   * Utility functions for common operations
   */
  const utils = {
    delay: ms => new Promise(resolve => setTimeout(resolve, ms)),
    querySelector: (selector, context = document) => context.querySelector(selector),
    querySelectorAll: (selector, context = document) => Array.from(context.querySelectorAll(selector)),
    hasClass: (element, className) => {
      if (!element) return false;
      
      // Check the element itself
      if (element.classList.contains(className)) return true;
      
      // Check parent elements
      let parent = element;
      while (parent) {
        if (parent.classList && parent.classList.contains(className)) {
          return true;
        }
        parent = parent.parentElement;
      }
      
      return false;
    }
  };

  /**
   * DOM Cache for frequently accessed elements
   */
  const domCache = {
    _queryBar: null,
    get queryBar() {
      if (!this._queryBar) {
        this._queryBar = utils.querySelector(CONFIG.selectors.queryBar);
      }
      return this._queryBar;
    },
    clearCache() {
      this._queryBar = null;
    }
  };

  /**
   * Event simulation utilities
   */
  const eventSimulator = {
    createMouseEvent(eventType) {
      return new MouseEvent(eventType, {
        bubbles: true,
        cancelable: true,
        view: window,
        clientX: CONFIG.mouse.clientX,
        clientY: CONFIG.mouse.clientY
      });
    },

    createKeyEvent(key, options = {}, type = 'keydown') {
      return new KeyboardEvent(type, {
        key,
        code: this._getKeyCode(key),
        bubbles: true,
        cancelable: true,
        keyCode: this._getKeyCodeNumber(key),
        ...options
      });
    },

    _getKeyCode(key) {
      return CONFIG.keyCodes[key]?.code || `Key${key.toUpperCase()}`;
    },

    _getKeyCodeNumber(key) {
      return CONFIG.keyCodes[key]?.keyCode || key.toUpperCase().charCodeAt(0);
    },

    async simulateMouseEvent(element, eventType) {
      element.dispatchEvent(this.createMouseEvent(eventType));
    },

    async simulateHover(element) {
      const parent = element.parentElement;
      if (parent) {
        ['mouseover', 'mouseenter'].forEach(event => this.simulateMouseEvent(parent, event));
        await utils.delay(CONFIG.delays.HOVER);
      }

      ['mouseover', 'mouseenter'].forEach(event => this.simulateMouseEvent(element, event));
      await utils.delay(CONFIG.delays.HOVER);
      
      element.classList.add('hover');
    },

    async simulateKeyPress(element, key, options = {}) {
      element.focus();
      await utils.delay(CONFIG.delays.KEY_PRESS);

      ['keydown', 'keyup'].forEach(async (type) => {
        element.dispatchEvent(this.createKeyEvent(key, options, type));
        document.dispatchEvent(this.createKeyEvent(key, options, type));
        await utils.delay(CONFIG.delays.KEY_PRESS);
      });
    }
  };

  /**
   * Chat operations handling
   */
  const chatOperations = {
    isValidInput(target) {
      return target.tagName === 'TEXTAREA' || 
        (!['INPUT'].includes(target.tagName) && !target.isContentEditable);
    },

    isPrivateChat() {
      const queryBar = domCache.queryBar;
      if (!queryBar) return false;

      // Check for purple classes either directly on the query bar or in its parent containers
      return CONFIG.selectors.privateClasses.some(className => utils.hasClass(queryBar, className));
    },

    clickHistoryButton() {
      const button = utils.querySelector(CONFIG.selectors.historyButton);
      if (button) {
        button.click();
        return true;
      }
      return false;
    },

    findSelectedChat(retryCount = 0) {
      const findElement = () => {
        const dialog = utils.querySelector(CONFIG.selectors.dialog);
        if (!dialog) return null;

        const currentSpan = utils.querySelectorAll('span', dialog)
          .find(span => span.textContent.trim() === CONFIG.text.current);
        if (!currentSpan) return null;

        return currentSpan.closest(CONFIG.selectors.cmdkItem);
      };

      return new Promise((resolve, reject) => {
        const element = findElement();
        if (element) {
          resolve(element);
        } else if (retryCount < CONFIG.maxRetries) {
          setTimeout(() => this.findSelectedChat(retryCount + 1).then(resolve).catch(reject), CONFIG.delays.RETRY);
        } else {
          reject(new Error('Selected chat not found'));
        }
      });
    },

    simulatePrivateChatShortcut() {
      return eventSimulator.simulateKeyPress(document.body, 'j', {
        metaKey: CONFIG.platform.isMac,
        ctrlKey: !CONFIG.platform.isMac,
        shiftKey: true
      });
    },

    async triggerDeleteSequence() {
      try {
        const currentChat = await this.findSelectedChat();
        const clickableArea = currentChat.querySelector(CONFIG.selectors.clickableArea);
        if (!clickableArea) throw new Error('Clickable area not found');

        await eventSimulator.simulateHover(currentChat);
        await eventSimulator.simulateHover(clickableArea);

        ['mousedown', 'click'].forEach(event => eventSimulator.simulateMouseEvent(clickableArea, event));
        await utils.delay(CONFIG.delays.CLICK_TO_DELETE);

        await eventSimulator.simulateKeyPress(clickableArea, 'd', {
          metaKey: CONFIG.platform.isMac,
          shiftKey: true
        });

        await utils.delay(CONFIG.delays.DELETE_TO_ENTER);
        await eventSimulator.simulateKeyPress(clickableArea, 'Enter');

        await utils.delay(CONFIG.delays.FINAL_ESCAPE);
        await eventSimulator.simulateKeyPress(document.body, 'Escape');

        return true;
      } catch {
        return false;
      }
    }
  };

  /**
   * Main keyboard event handler
   */
  const handleKeyDown = (e) => {
    if (!chatOperations.isValidInput(document.activeElement)) return;
    
    if (e.key === CONFIG.shortcut.key && 
        e[CONFIG.shortcut.modifier] && 
        e.shiftKey && 
        !e.altKey) {
      e.preventDefault();
      e.stopPropagation();
      
      if (chatOperations.isPrivateChat()) {
        chatOperations.simulatePrivateChatShortcut();
      } else if (chatOperations.clickHistoryButton()) {
        setTimeout(chatOperations.triggerDeleteSequence.bind(chatOperations), CONFIG.delays.MENU_OPEN);
      }
    }
  };

  /**
   * Initialize the script
   */
  const init = () => {
    document.addEventListener('keydown', handleKeyDown);
    window.addEventListener('popstate', () => domCache.clearCache());
  };

  init();
})();