// ==UserScript==
// @name Gerenciador de chats ChatGPT
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Gerenciamento em massa de conversas em plataformas de IA
// @author luascfl
// @match https://chat.openai.com/*
// @match https://chatgpt.com/*
// @icon https://cdn-icons-png.flaticon.com/512/16459/16459818.png
// @home https://github.com/luascfl/manage-chats-chatgpt
// @supportURL https://github.com/luascfl/manage-chats-chatgpt/issues
// @license MIT
// @grant none
// ==/UserScript==
(function() {
'use strict';
/**
* Configuração específica para cada plataforma
* Facilita a adaptação para outras plataformas de IA no futuro
*/
const PLATFORMS = {
'chat.openai.com': {
name: 'ChatGPT',
selectors: {
chatList: '.flex.flex-col.gap-2.text-token-text-primary',
chatItems: 'ol > li',
chatLink: 'a[href^="/c/"]',
chatTitle: 'div[dir="auto"]'
},
api: {
base: window.location.origin,
tokenEndpoint: '/api/auth/session',
conversationEndpoint: '/backend-api/conversation/',
tokenExtractor: (data) => data.accessToken
},
priorityEmoji: '❗'
},
// Modelo para adicionar outras plataformas
/*
'exemplo.com': {
name: 'Nome da Plataforma',
selectors: {
chatList: '.seletor-lista-chats',
chatItems: '.seletor-item-chat',
chatLink: '.seletor-link-chat',
chatTitle: '.seletor-titulo-chat'
},
api: {
base: 'https://api.exemplo.com',
tokenEndpoint: '/auth/token',
conversationEndpoint: '/api/conversations/',
tokenExtractor: (data) => data.token
},
priorityEmoji: '⭐'
}
*/
};
// Detecta a plataforma atual
const getCurrentPlatform = () => {
const hostname = window.location.hostname;
return PLATFORMS[hostname] || PLATFORMS['chat.openai.com']; // Padrão para ChatGPT
};
const PLATFORM = getCurrentPlatform();
const API_BASE = PLATFORM.api.base;
const SELECTOR = PLATFORM.selectors;
const PRIORITY_EMOJI = PLATFORM.priorityEmoji;
/**
* Gerenciador de UI
*/
class UIManager {
constructor() {
this.addStyles();
}
addStyles() {
const styleEl = document.createElement('style');
styleEl.innerHTML = `
.mass-actions {
background-color: var(--surface-primary);
padding: 10px;
border-radius: 8px;
margin-bottom: 16px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.mass-actions-title {
font-weight: bold;
margin-bottom: 10px;
font-size: 14px;
color: var(--text-primary);
}
.mass-actions-btn {
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: 1px solid var(--border-primary);
margin-bottom: 5px;
}
.mass-actions-btn:hover {
opacity: 0.9;
}
.btn-select-all {
background-color: var(--surface-secondary);
}
.btn-deselect-all {
background-color: var(--surface-secondary);
}
.btn-select-without-emoji {
background-color: var(--surface-secondary);
}
.btn-archive {
background-color: var(--surface-tertiary);
}
.btn-delete {
background-color: rgba(255, 76, 76, 0.1);
color: #ff4c4c;
}
.checkbox-container {
position: absolute;
left: 8px;
top: 0;
bottom: 0;
display: flex;
align-items: center;
z-index: 10;
}
.dialog-checkbox {
cursor: pointer;
width: 16px;
height: 16px;
}
.chat-item-container {
position: relative;
}
.chat-link-padded {
padding-left: 30px !important;
}
.chat-action-status {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 16px;
background: var(--surface-primary);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
display: flex;
align-items: center;
font-size: 14px;
}
.status-icon {
margin-right: 8px;
font-size: 18px;
}
.status-success {
color: #4caf50;
}
.status-error {
color: #f44336;
}
.status-loading {
color: #2196f3;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-spinner {
animation: spin 1s linear infinite;
display: inline-block;
}
.select-count {
margin-left: 8px;
font-size: 13px;
color: var(--text-secondary);
}
`;
document.head.appendChild(styleEl);
}
showStatus(message, type = 'loading') {
// Remove qualquer status existente
const existingStatus = document.querySelector('.chat-action-status');
if (existingStatus) existingStatus.remove();
const statusEl = document.createElement('div');
statusEl.className = 'chat-action-status';
let icon = '';
if (type === 'loading') {
icon = '<span class="status-icon status-loading"><span class="loading-spinner">⟳</span></span>';
} else if (type === 'success') {
icon = '<span class="status-icon status-success">✓</span>';
} else if (type === 'error') {
icon = '<span class="status-icon status-error">✕</span>';
}
statusEl.innerHTML = `${icon}${message}`;
document.body.appendChild(statusEl);
if (type !== 'loading') {
setTimeout(() => {
statusEl.remove();
}, 3000);
}
return statusEl;
}
updateSelectedCount() {
const selectedCount = document.querySelectorAll('.dialog-checkbox:checked').length;
const countElement = document.querySelector('.selected-count');
if (countElement) {
countElement.textContent = selectedCount > 0 ? `${selectedCount} selecionado${selectedCount > 1 ? 's' : ''}` : '';
}
}
createCheckbox(chatItem) {
// Verifica se já existe um checkbox
if (chatItem.querySelector('.checkbox-container')) return;
// Adiciona classe ao container do chat
chatItem.classList.add('chat-item-container');
// Encontra o link de chat
const chatLink = chatItem.querySelector(SELECTOR.chatLink);
if (!chatLink) return;
// Adiciona classe ao link para dar espaço ao checkbox
chatLink.classList.add('chat-link-padded');
// Cria container do checkbox
const checkboxContainer = document.createElement('div');
checkboxContainer.className = 'checkbox-container';
// Cria o checkbox
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'dialog-checkbox';
checkbox.addEventListener('change', () => this.updateSelectedCount());
checkbox.addEventListener('click', (e) => {
e.stopPropagation(); // Impede que o clique no checkbox navegue para o chat
});
// Adiciona checkbox ao container
checkboxContainer.appendChild(checkbox);
// Adiciona o container ao item de chat
chatItem.appendChild(checkboxContainer);
}
ensureCorrectCheckboxes() {
document.querySelectorAll(SELECTOR.chatItems).forEach(chatItem => {
// Remove qualquer checkbox antigo que possa estar dentro do link
const oldCheckbox = chatItem.querySelector('a .dialog-checkbox');
if (oldCheckbox) {
oldCheckbox.remove();
}
// Cria um novo checkbox corretamente posicionado
this.createCheckbox(chatItem);
});
}
setupControlPanel() {
const chatList = document.querySelector(SELECTOR.chatList);
if (!chatList || chatList.querySelector('.mass-actions')) return;
const controls = document.createElement('div');
controls.className = 'mass-actions';
controls.innerHTML = `
<div class="mass-actions-title">Gerenciamento em massa</div>
<div class="flex gap-2 flex-wrap">
<button class="mass-actions-btn btn-select-all">Selecionar tudo</button>
<button class="mass-actions-btn btn-select-without-emoji">Selecionar sem ${PRIORITY_EMOJI}</button>
<button class="mass-actions-btn btn-deselect-all">Desmarcar tudo</button>
<button class="mass-actions-btn btn-archive">Arquivar selecionados</button>
<button class="mass-actions-btn btn-delete">Excluir selecionados</button>
<span class="selected-count select-count"></span>
</div>
`;
// Configura os eventos dos botões
this.setupButtonHandlers(controls);
chatList.prepend(controls);
// Adiciona checkboxes a todos os itens de chat existentes
document.querySelectorAll(SELECTOR.chatItems).forEach(chatItem => this.createCheckbox(chatItem));
this.updateSelectedCount();
}
setupButtonHandlers(controls) {
controls.querySelector('.btn-select-all').addEventListener('click', () => {
document.querySelectorAll('.dialog-checkbox').forEach(cb => cb.checked = true);
this.updateSelectedCount();
});
controls.querySelector('.btn-select-without-emoji').addEventListener('click', () => {
chatManager.selectChatsWithoutPriorityEmoji();
this.updateSelectedCount();
});
controls.querySelector('.btn-deselect-all').addEventListener('click', () => {
document.querySelectorAll('.dialog-checkbox').forEach(cb => cb.checked = false);
this.updateSelectedCount();
});
controls.querySelector('.btn-archive').addEventListener('click', () => {
if (confirm('Deseja arquivar todas as conversas selecionadas?')) {
chatManager.updateChats({ is_archived: true });
}
});
controls.querySelector('.btn-delete').addEventListener('click', () => {
if (confirm('Deseja excluir todas as conversas selecionadas? Esta ação não pode ser desfeita.')) {
chatManager.updateChats({ is_visible: false });
}
});
}
}
/**
* Gerenciador de Chats
*/
class ChatManager {
constructor(uiManager) {
this.ui = uiManager;
}
async getAccessToken() {
try {
const response = await fetch(`${API_BASE}${PLATFORM.api.tokenEndpoint}`);
const data = await response.json();
return PLATFORM.api.tokenExtractor(data);
} catch (error) {
console.error('Erro ao obter token:', error);
return null;
}
}
getChatId(element) {
const chatItem = element.closest('li');
const link = chatItem.querySelector(SELECTOR.chatLink);
return link ? new URL(link.href).pathname.split('/').pop() : null;
}
hasPriorityEmoji(chatItem) {
const link = chatItem.querySelector(SELECTOR.chatLink);
if (!link) return false;
const titleDiv = link.querySelector(SELECTOR.chatTitle);
return titleDiv && titleDiv.textContent.includes(PRIORITY_EMOJI);
}
selectChatsWithoutPriorityEmoji() {
const chatItems = document.querySelectorAll(SELECTOR.chatItems);
chatItems.forEach(chatItem => {
const checkbox = chatItem.querySelector('.dialog-checkbox');
if (checkbox) {
// Marca o checkbox apenas se NÃO tiver o emoji de prioridade
checkbox.checked = !this.hasPriorityEmoji(chatItem);
}
});
}
async updateChats(body) {
const checkboxes = document.querySelectorAll('.dialog-checkbox:checked');
if (checkboxes.length === 0) {
this.ui.showStatus('Nenhuma conversa selecionada', 'error');
return;
}
const action = body.is_archived ? 'arquivando' : 'excluindo';
const statusEl = this.ui.showStatus(`${action.charAt(0).toUpperCase() + action.slice(1)} ${checkboxes.length} conversas...`);
const accessToken = await this.getAccessToken();
if (!accessToken) {
this.ui.showStatus('Token de acesso não encontrado', 'error');
return;
}
try {
let processed = 0;
await Promise.all(Array.from(checkboxes).map(async (checkbox) => {
const chatId = this.getChatId(checkbox);
if (!chatId) return;
const response = await fetch(`${API_BASE}${PLATFORM.api.conversationEndpoint}${chatId}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
checkbox.closest('li').style.opacity = '0.5';
processed++;
// Atualizar status com progresso
statusEl.innerHTML = `<span class="status-icon status-loading"><span class="loading-spinner">⟳</span></span>${action.charAt(0).toUpperCase() + action.slice(1)} conversas... (${processed}/${checkboxes.length})`;
}));
this.ui.showStatus(`${processed} conversas ${body.is_archived ? 'arquivadas' : 'excluídas'} com sucesso!`, 'success');
// Recarregar a página após um breve atraso para mostrar o status
setTimeout(() => {
window.location.reload();
}, 1500);
} catch (error) {
console.error('Erro ao processar conversas:', error);
this.ui.showStatus(`Erro ao processar conversas: ${error.message}`, 'error');
}
}
}
/**
* Classe principal que orquestra tudo
*/
class ChatManagerApp {
constructor() {
this.uiManager = new UIManager();
this.chatManager = new ChatManager(this.uiManager);
// Expõe o chatManager para uso nos event handlers
window.chatManager = this.chatManager;
}
init() {
// Inicialização com delay para garantir que a página carregou completamente
setTimeout(() => {
this.uiManager.setupControlPanel();
this.uiManager.ensureCorrectCheckboxes();
this.setupObserver();
}, 1000);
}
setupObserver() {
// Observador para detectar mudanças na lista de chats
const observer = new MutationObserver((mutations) => {
const chatList = document.querySelector(SELECTOR.chatList);
if (chatList) {
this.uiManager.setupControlPanel();
this.uiManager.ensureCorrectCheckboxes();
// Adiciona checkboxes a novos itens
mutations.forEach(mutation => {
if (mutation.addedNodes.length) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1 && node.matches(SELECTOR.chatItems)) {
this.uiManager.createCheckbox(node);
} else if (node.nodeType === 1) {
node.querySelectorAll(SELECTOR.chatItems).forEach(item =>
this.uiManager.createCheckbox(item)
);
}
});
}
});
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}
// Inicializa a aplicação
const app = new ChatManagerApp();
app.init();
})();