Faster ChatGPT Delete

Hover to reveal a trash‑can icon and click to auto‑delete the conversation (there is no confirmation popup).

  1. // ==UserScript==
  2. // @name Faster ChatGPT Delete
  3. // @namespace https://chat.openai.com/
  4. // @version 1.0
  5. // @description Hover to reveal a trash‑can icon and click to auto‑delete the conversation (there is no confirmation popup).
  6. // @match https://chat.openai.com/*
  7. // @include https://chatgpt.com/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (() => {
  13. const waitFor = (pred, ms = 4000, step = 70) =>
  14. new Promise(res => {
  15. const end = Date.now() + ms;
  16. (function loop() {
  17. const el = pred();
  18. if (el) return res(el);
  19. if (Date.now() > end) return res(null);
  20. setTimeout(loop, step);
  21. })();
  22. });
  23.  
  24. const fire = (el, type) =>
  25. el.dispatchEvent(new MouseEvent(type, { bubbles: true, composed: true }));
  26.  
  27. async function deleteConversation(anchor) {
  28. const href = anchor.getAttribute('href');
  29. const stay = href && location.pathname !== href;
  30.  
  31. const dots = anchor.querySelector('button[data-testid$="-options"]');
  32. if (!dots) return;
  33. ['pointerdown', 'pointerup', 'click'].forEach(t => fire(dots, t));
  34.  
  35. const del = await waitFor(() =>
  36. [...document.querySelectorAll('[role="menuitem"], button')].find(el =>
  37. /^delete$/i.test(el.textContent.trim()) && !el.closest('.quick‑delete')
  38. )
  39. );
  40. if (!del) return;
  41. ['pointerdown', 'pointerup', 'click'].forEach(t => fire(del, t));
  42.  
  43. const confirm = await waitFor(() =>
  44. document.querySelector('button[data-testid="delete-conversation-confirm-button"], .btn-danger')
  45. );
  46. if (!confirm) return;
  47. ['pointerdown', 'pointerup', 'click'].forEach(t => fire(confirm, t));
  48.  
  49. if (stay) setTimeout(() => history.replaceState(null, '', location.pathname), 80);
  50.  
  51. anchor.style.transition = 'opacity .25s';
  52. anchor.style.opacity = '0';
  53. setTimeout(() => (anchor.style.display = 'none'), 280);
  54. }
  55.  
  56. const ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
  57. viewBox="0 0 24 24" fill="none" stroke="currentColor"
  58. stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  59. <polyline points="3 6 5 6 21 6"></polyline>
  60. <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6
  61. m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
  62. <line x1="10" y1="11" x2="10" y2="17"></line>
  63. <line x1="14" y1="11" x2="14" y2="17"></line></svg>`;
  64.  
  65. function decorate(anchor) {
  66. if (anchor.querySelector('.quick‑delete')) return;
  67.  
  68. anchor.style.position = 'relative';
  69.  
  70. const icon = Object.assign(document.createElement('span'), {
  71. className: 'quick‑delete',
  72. innerHTML: ICON
  73. });
  74.  
  75. const bg1 = 'var(--sidebar-surface-secondary, #4b5563)';
  76. const bg2 = 'var(--sidebar-surface-tertiary , #6b7280)';
  77.  
  78. Object.assign(icon.style, {
  79. position: 'absolute',
  80. left: '4px',
  81. top: '50%',
  82. transform: 'translateY(-50%)',
  83. cursor: 'pointer',
  84. pointerEvents: 'auto',
  85. zIndex: 5,
  86. padding: '2px',
  87. borderRadius: '4px',
  88. background: `linear-gradient(135deg, ${bg1}, ${bg2})`,
  89. color: 'var(--token-text-primary)',
  90. opacity: 0,
  91. transition: 'opacity 100ms'
  92. });
  93.  
  94. anchor.addEventListener('mouseenter', () => {
  95. icon.style.opacity = '.85';
  96. anchor.style.transition = 'padding-left 100ms';
  97. anchor.style.paddingLeft = '28px';
  98. });
  99. anchor.addEventListener('mouseleave', () => {
  100. icon.style.opacity = '0';
  101. anchor.style.paddingLeft = '';
  102. });
  103.  
  104. icon.addEventListener('click', e => {
  105. e.stopPropagation();
  106. e.preventDefault();
  107. deleteConversation(anchor);
  108. });
  109.  
  110. anchor.prepend(icon);
  111. }
  112.  
  113. const itemSelector = 'a.__menu-item';
  114.  
  115. function handleMutation(records) {
  116. for (const rec of records) {
  117. rec.addedNodes.forEach(node => {
  118. if (node.nodeType === 1 && node.matches(itemSelector)) decorate(node);
  119. else if (node.nodeType === 1) node.querySelectorAll?.(itemSelector).forEach(decorate);
  120. });
  121. }
  122. }
  123.  
  124. function decorateInBatches(nodes) {
  125. const batch = nodes.splice(0, 50);
  126. batch.forEach(decorate);
  127. if (nodes.length) requestIdleCallback(() => decorateInBatches(nodes));
  128. }
  129.  
  130. function init() {
  131. const container = document.querySelector('nav') || document.body;
  132. new MutationObserver(handleMutation)
  133. .observe(container, { childList: true, subtree: true });
  134. const startNodes = [...container.querySelectorAll(itemSelector)];
  135. if (startNodes.length) requestIdleCallback(() => decorateInBatches(startNodes));
  136. }
  137.  
  138. const ready = setInterval(() => {
  139. if (document.querySelector('nav')) {
  140. clearInterval(ready);
  141. init();
  142. }
  143. }, 200);
  144. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址