ChatGPT 바로 삭제

ChatGPT 대화목록에 마우스를 올리면 휴지통 아이콘이 나타나고, 클릭 시 확인창 없이 대화를 즉시 삭제합니다.

// ==UserScript==
// @name         ChatGPT 바로 삭제
// @namespace    https://chatgpt.com/
// @version      1.0.0
// @description  ChatGPT 대화목록에 마우스를 올리면 휴지통 아이콘이 나타나고, 클릭 시 확인창 없이 대화를 즉시 삭제합니다.
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @grant        none
// @license      CC-BY-NC-SA-4.0
// @author       guvno
// @source       https://greasyfork.org/en/scripts/533597-chatgpt-quick-delete-no-popup
// ==/UserScript==

(() => {
  const DEBUG = false; // true로 설정 시 console.log 출력

  const waitFor = (pred, ms = 8000, step = 70) =>
    new Promise(res => {
      const end = Date.now() + ms;
      (function loop() {
        const el = pred();
        if (el) return res(el);
        if (Date.now() > end) return res(null);
        setTimeout(loop, step);
      })();
    });

  const fire = (el, type) =>
    el.dispatchEvent(new MouseEvent(type, { bubbles: true, composed: true }));

  async function deleteConversation(li) {
    const dots = li.querySelector(
      'button[data-testid$="-options"], button[aria-label="대화 옵션 열기"]'
    );
    if (DEBUG) console.log('[QD] options button:', dots);
    if (!dots) return;
    ['pointerdown', 'pointerup', 'click'].forEach(t => fire(dots, t));

    await waitFor(() => document.querySelector('[role="menu"]'), 8000);
    const menuItems = [...document.querySelectorAll('[role="menuitem"], button')];
    if (DEBUG) console.log('[QD] all menu items:', menuItems.map(el => el.textContent.trim()));

    const del = menuItems.find(el => /삭제/.test(el.textContent));
    if (DEBUG) console.log('[QD] matched delete item:', del);
    if (!del) return;
    ['pointerdown', 'pointerup', 'click'].forEach(t => fire(del, t));

    const confirm = await waitFor(() => {
      return (
        document.querySelector('button[data-testid="delete-conversation-confirm-button"]') ||
        [...document.querySelectorAll('button')].find(el =>
          el.textContent.trim() === '삭제' || el.textContent.includes('삭제')
        )
      );
    }, 8000);
    if (DEBUG) console.log('[QD] confirm button:', confirm);
    if (!confirm) return;
    ['pointerdown', 'pointerup', 'click'].forEach(t => fire(confirm, t));

    li.style.transition = 'opacity .25s';
    li.style.opacity = 0;
    setTimeout(() => li.style.display = 'none', 280);
  }

  const ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
      viewBox="0 0 24 24" fill="none" stroke="currentColor"
      stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <polyline points="3 6 5 6 21 6"></polyline>
      <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6
               m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
      <line x1="10" y1="11" x2="10" y2="17"></line>
      <line x1="14" y1="11" x2="14" y2="17"></line></svg>`;

  function decorate(li) {
    if (li.querySelector('.quick‑delete')) return;

    const grp = li.querySelector('.group');
    const link = grp?.querySelector('a[data-history-item-link]');
    if (!grp || !link) return;
    grp.style.position = 'relative';

    if (!link.dataset.origPad) {
      link.dataset.origPad = getComputedStyle(link).paddingLeft || '0px';
    }

    const icon = Object.assign(document.createElement('span'), {
      className: 'quick‑delete',
      innerHTML: ICON
    });

    const bg1 = 'var(--sidebar-surface-secondary, #4b5563)';
    const bg2 = 'var(--sidebar-surface-tertiary , #6b7280)';

    Object.assign(icon.style, {
      position: 'absolute',
      left: '4px',
      top: '50%',
      transform: 'translateY(-50%)',
      cursor: 'pointer',
      pointerEvents: 'auto',
      zIndex: 5,
      padding: '2px',
      borderRadius: '4px',
      background: `linear-gradient(135deg, ${bg1}, ${bg2})`,
      color: 'var(--token-text-primary)',
      opacity: 0,
      transition: 'opacity 100ms'
    });

    grp.addEventListener('mouseenter', () => {
      icon.style.opacity = '.85';
      link.style.transition = 'padding-left 100ms';
      link.style.paddingLeft = '28px';
    });
    grp.addEventListener('mouseleave', () => {
      icon.style.opacity = '0';
      link.style.paddingLeft = link.dataset.origPad;
    });

    icon.addEventListener('click', e => {
      e.stopPropagation();
      e.preventDefault();
      deleteConversation(li);
    });

    grp.prepend(icon);
  }

  const itemSelector = 'li[data-testid^="history-item-"]';

  function handleMutation(records) {
    for (const rec of records) {
      rec.addedNodes.forEach(node => {
        if (node.nodeType === 1 && node.matches(itemSelector)) decorate(node);
        else if (node.nodeType === 1) node.querySelectorAll?.(itemSelector).forEach(decorate);
      });
    }
  }

  function decorateInBatches(nodes) {
    const batch = nodes.splice(0, 50);
    batch.forEach(decorate);
    if (nodes.length) requestIdleCallback(() => decorateInBatches(nodes));
  }

  function init() {
    const history = document.getElementById('history');
    if (!history) return;
    new MutationObserver(handleMutation)
      .observe(history, { childList: true, subtree: true });
    const startNodes = [...history.querySelectorAll(itemSelector)];
    if (startNodes.length) requestIdleCallback(() => decorateInBatches(startNodes));
  }

  const ready = setInterval(() => {
    if (document.getElementById('history')) {
      clearInterval(ready);
      init();
    }
  }, 150);
})();