// ==UserScript==
// @name ChatGPT Bulk & Quick Delete
// @namespace https://kurotaku.de
// @version 1.0
// @description Easily delete multiple chats in bulk or quickly remove individual chats with Shift + Hover. You can also export a single chat as a TXT file.
// @author Kurotaku
// @license CC BY-NC-SA 4.0
// @match https://chatgpt.com/*
// @updateURL https://raw.githubusercontent.com/Kurotaku-sama/Userscripts/main/userscripts/ChatGPT_Bulk_and_Quick_Delete/script.user.js
// @downloadURL https://raw.githubusercontent.com/Kurotaku-sama/Userscripts/main/userscripts/ChatGPT_Bulk_and_Quick_Delete/script.user.js
// @require https://raw.githubusercontent.com/Kurotaku-sama/Userscripts/main/libraries/kuros_library.js
// @require https://cdn.jsdelivr.net/npm/sweetalert2
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// ==/UserScript==
// --------------------------
// Globals
// --------------------------
let shift_pressed = false;
let selection_mode = false;
let selection_observer = null;
let auth_token = null;
let selected_chats = new Set();
let quick_delete_locks = new Set();
(async function() {
await init_gm_config();
GM_registerMenuCommand("Export current Chat as TXT", export_current_chat_as_txt);
if (GM_config.get("script_enabled")) {
if (GM_config.get("enable_bulk_deletion"))
ensure_bulk_delete_panel();
if (GM_config.get("enable_quick_deletion"))
init_quick_delete_listeners();
}
})();
async function init_gm_config() {
const config_id = "configuration_chatgpt_baqd";
await migrate_config_id(config_id);
GM_registerMenuCommand("Settings", () => GM_config.open());
GM_config.init({
id: config_id,
title: 'ChatGPT Bulk and Quick Delete',
fields: {
script_enabled: {
type: 'checkbox',
default: true,
label: 'Enable/Disable the Script',
},
enable_bulk_deletion: {
section: ['Deletion Options'],
type: 'checkbox',
default: true,
label: 'Enable Bulk Deletion',
},
enable_quick_deletion: {
type: 'checkbox',
default: true,
label: 'Enable Quick Deletion (Shift + Hover)',
},
},
events: {
save: () => { location.reload() },
},
frame: create_configuration_container(),
});
await wait_for_gm_config();
}
// --------------------------
// Quick Delete Logic
// --------------------------
async function init_quick_delete_listeners() {
const history_container = await wait_for_element("#history");
if (!history_container) return;
// Track the hovered chat element
let hovered_chat = null;
// Mouseenter / leave für alle a[href^="/c/"]
history_container.addEventListener('mouseover', e => {
const target = e.target.closest('a[href^="/c/"]');
if (target) hovered_chat = target;
});
history_container.addEventListener('mouseout', e => {
const target = e.target.closest('a[href^="/c/"]');
if (target && target === hovered_chat) hovered_chat = null;
});
// Shift Listener only active if hovered_chat exists
document.addEventListener('keydown', e => {
if (e.key === 'Shift' && !shift_pressed && hovered_chat) {
shift_pressed = true;
toggle_quick_delete(true);
}
});
document.addEventListener('keyup', e => {
if (e.key === 'Shift' && shift_pressed) {
shift_pressed = false;
toggle_quick_delete(false);
}
});
}
function toggle_quick_delete(enable) {
const chat_container = document.querySelector('#history');
if (!chat_container) return;
chat_container.querySelectorAll('a[href^="/c/"]').forEach(chat => {
chat.onmouseenter = null;
chat.onmouseleave = null;
const show_trash = () => {
if (!shift_pressed) return;
const menu_btn = chat.querySelector('button.__menu-item-trailing-btn');
if (menu_btn) menu_btn.style.display = 'none';
let trash_btn = chat.querySelector('.quick-delete-btn');
if (!trash_btn) {
const trash_html = `
`;
chat.insertAdjacentHTML('beforeend', trash_html);
trash_btn = chat.querySelector('.quick-delete-btn');
trash_btn.addEventListener('click', async e => {
e.stopPropagation();
e.preventDefault();
const href = chat.getAttribute('href');
if (quick_delete_locks.has(href)) return; // Already deleting
quick_delete_locks.add(href); // Lock
try {
await delete_chat(chat);
chat.remove();
} catch (err) {
await show_error_message({ title: "Failed to delete chat", text: err.error?.message || "Unknown error", chat_element: chat });
} finally {
quick_delete_locks.delete(href); // Unlock
}
});
}
trash_btn.style.display = 'inline-block';
};
const hide_trash = () => {
const menu_btn = chat.querySelector('button.__menu-item-trailing-btn');
if (menu_btn) menu_btn.style.display = 'inline-block';
const trash_btn = chat.querySelector('.quick-delete-btn');
if (trash_btn) trash_btn.style.display = 'none';
};
chat.onmouseenter = show_trash;
chat.onmouseleave = hide_trash;
if (enable && chat.matches(':hover')) show_trash();
if (!enable) hide_trash();
});
}
// --------------------------
// Selection / Bulk Delete Logic
// --------------------------
function toggle_selection_mode() {
selection_mode = !selection_mode;
const toggle_btn = document.getElementById('btn-bulk-delete');
const delete_btn = document.getElementById('btn-delete-number');
const cancel_btn = document.getElementById('btn-cancel-bulk');
const panel = document.getElementById('panel-bulk-delete');
const chat_container = document.querySelector('#history');
if (!chat_container) return;
const add_listeners_to_chats = () => {
const chat_items = chat_container.querySelectorAll('a[href^="/c/"]');
chat_items.forEach(chat => {
if (!chat.classList.contains('chat-selectable')) {
chat.classList.add('chat-selectable');
chat.addEventListener('click', handle_chat_click, true);
}
});
}
if (selection_mode) {
toggle_btn.style.display = 'none';
panel.style.display = 'flex';
// bereits existierende Chats markieren
add_listeners_to_chats();
// Observer für nachgeladene Chats starten
selection_observer = new MutationObserver(() => add_listeners_to_chats());
selection_observer.observe(chat_container, { childList: true });
} else {
panel.style.display = 'none';
toggle_btn.style.display = 'inline-block';
// EventListener entfernen und Klassen aufräumen
const chat_items = chat_container.querySelectorAll('a[href^="/c/"]');
chat_items.forEach(chat => {
chat.classList.remove('chat-selectable', 'chat-selected');
chat.removeEventListener('click', handle_chat_click, true);
});
selected_chats.clear();
update_delete_button();
// Observer stoppen
if (selection_observer) {
selection_observer.disconnect();
selection_observer = null;
}
}
}
function handle_chat_click(event) {
event.preventDefault();
event.stopPropagation();
const chat_element = event.currentTarget;
if (selected_chats.has(chat_element)) {
selected_chats.delete(chat_element);
chat_element.classList.remove('chat-selected');
} else {
selected_chats.add(chat_element);
chat_element.classList.add('chat-selected');
}
update_delete_button();
}
function update_delete_button() {
const delete_btn = document.getElementById('btn-delete-number');
if (delete_btn) {
delete_btn.textContent = `Delete ${selected_chats.size}`;
delete_btn.disabled = selected_chats.size === 0;
}
}
// --------------------------
// Bulk Delete Actions
// --------------------------
async function delete_selected_chats() {
if (selected_chats.size === 0) return;
const { isConfirmed } = await Swal.fire({
title: `Delete ${selected_chats.size} chats?`,
text: "This action cannot be undone!",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Delete",
cancelButtonText: "Cancel",
backdrop: true,
theme: "dark",
});
if (!isConfirmed) return;
const chats_to_delete = Array.from(selected_chats);
const total = chats_to_delete.length;
const delete_btn = document.getElementById("btn-delete-number");
const cancel_btn = document.getElementById("btn-cancel-bulk");
delete_btn.disabled = true;
cancel_btn.disabled = true;
Swal.fire({
title: "Deleting chats...",
html: `
0/${total}`,
showConfirmButton: false,
allowOutsideClick: false,
backdrop: true,
theme: "dark",
willOpen: () => Swal.showLoading(),
});
let success_count = 0;
let error_count = 0;
let aborted = false;
for (let i = 0; i < chats_to_delete.length; i++) {
try {
await delete_chat(chats_to_delete[i]);
chats_to_delete[i].style.transition = "opacity 0.5s";
chats_to_delete[i].style.opacity = "0";
await sleep_s(1);
chats_to_delete[i].remove();
success_count++;
} catch (err) {
// Only abort on "No auth token"
if (err.error?.message?.includes("No auth token")) {
await show_error_message({
title: "Auth Error",
text: err.error.message,
chat_element: err.chat_element
});
aborted = true;
break;
}
// Other errors just count as failure and continue
console.error(`[Delete Error] ${err.chat_title}: ${err.error?.message || "Unknown error"}`);
error_count++;
continue; // continue with next chat
}
Swal.update({
html: `
${i+1}/${total}`
});
}
// Only show if not aborted
if (!aborted) {
Swal.close()
let final_icon = "success";
if (success_count === 0 && error_count > 0) final_icon = "error";
else if (success_count > 0 && error_count > 0) final_icon = "warning";
// Dynamisch HTML zusammenbauen
let html_parts = [];
if (success_count > 0)
html_parts.push(`Successfully deleted: ${success_count}`);
if (error_count > 0)
html_parts.push(`Failed: ${error_count}`);
const html_content = html_parts.join('
');
Swal.fire({
title: "Deletion finished",
html: html_content,
icon: final_icon,
backdrop: true,
theme: "dark",
});
}
delete_btn.disabled = false;
cancel_btn.disabled = false;
if (selection_mode) toggle_selection_mode();
}
// --------------------------
// Delete Function
// --------------------------
async function delete_chat(chat_element) {
// Ensure we have an auth token
if (!auth_token) {
const token_ok = await get_auth_token()
if (!token_ok)
throw {
chat_element,
chat_title: chat_element.textContent.trim(),
error: new Error("No auth token")
}
}
const href = chat_element.getAttribute('href')
const conversation_id = href.split('/').pop()
const chat_title = chat_element.textContent.trim()
try {
const response = await fetch(`https://chatgpt.com/backend-api/conversation/${conversation_id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${auth_token}`
},
body: JSON.stringify({ is_visible: false })
})
if (!response.ok)
throw new Error(`Status ${response.status}`)
return { chat_element }
} catch (error) {
throw { chat_element, chat_title, error }
}
}
// --------------------------
// UI Initialization
// --------------------------
async function ensure_bulk_delete_panel() {
let bulk_panel_observer_started = false;
while (true) {
// Warte zuerst auf mindestens 1 Chat
await wait_for_element('#history > a[href^="/c/"]');
// Dann Sidebar Header abwarten
const container = await wait_for_element("#sidebar-header div.flex");
// Panel einfügen
const html = `