Outlier Playground Sound Notification

Toca um som quando a geração de resposta termina, adiciona "Continue" na caixa de texto e clica em "Not now" quando detectado

// ==UserScript==
// @name         Outlier Playground Sound Notification
// @namespace    http://tampermonkey.net/
// @version      5.0
// @description  Toca um som quando a geração de resposta termina, adiciona "Continue" na caixa de texto e clica em "Not now" quando detectado
// @author       luascfl (revisado por Gemini e Claude)
// @match        https://app.outlier.ai/playground*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=outlier.ai
// @license      MIT
// @homepageURL  https://github.com/luascfl/outlier-playground-sound-notification
// @supportURL   https://github.com/luascfl/outlier-playground-sound-notification/issues
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- CONFIGURAÇÃO ---
    const SOUND_URL = "https://od.lk/s/MjJfMzM5NTM3ODNf/331673__nicola_ariutti__brass_bell_01_take10.wav";
    const POLLING_INTERVAL_MS = 200; // Intervalo de verificação em milissegundos. 200ms é um bom equilíbrio.
    const AUTO_CONTINUE_TEXT = "Continue"; // Texto a ser adicionado automaticamente
    const NOT_NOW_CHECK_INTERVAL_MS = 500; // Intervalo para verificar se o botão está clicável
    const NOT_NOW_MAX_WAIT_MS = 30000; // Tempo máximo de espera (30 segundos)

    // --- INICIALIZAÇÃO ---
    const audio = new Audio(SOUND_URL);
    let lastState = null;
    let currentNotNowMonitor = null; // Armazena o ID do intervalo atual do monitor

    console.log("🚀 Iniciando Outlier Playground Sound Notification v5.0...");

    /**
     * Tenta tocar o som de notificação.
     * Inclui tratamento de erro para casos em que o navegador bloqueia o autoplay.
     */
    function playSound() {
        console.log("🔔 Tocando som de notificação...");
        audio.play().catch(e => console.error("Erro ao tocar o som. O navegador pode ter bloqueado a reprodução automática. Interaja com a página (clique em algo) e tente novamente.", e));
    }

    /**
     * Adiciona texto "Continue" na caixa de texto do prompt simulando interação real do usuário
     */
    function addContinueText() {
        const textArea = document.querySelector('textarea.ChatInput_textarea__QUOCH');
        
        if (textArea) {
            // Limpa e foca
            textArea.focus();
            textArea.select();
            document.execCommand('delete');
            
            // Método 1: Usa execCommand para inserir texto (funciona em muitos casos onde outros métodos falham)
            document.execCommand('insertText', false, AUTO_CONTINUE_TEXT);
            
            // Método 2: Simula eventos de teclado para cada caractere
            setTimeout(() => {
                if (textArea.value !== AUTO_CONTINUE_TEXT) {
                    textArea.value = '';
                    textArea.focus();
                    
                    // Simula pressionamento de tecla para cada caractere
                    for (let i = 0; i < AUTO_CONTINUE_TEXT.length; i++) {
                        const char = AUTO_CONTINUE_TEXT[i];
                        
                        // KeyDown
                        const keydownEvent = new KeyboardEvent('keydown', {
                            key: char,
                            code: 'Key' + char.toUpperCase(),
                            keyCode: char.charCodeAt(0),
                            which: char.charCodeAt(0),
                            bubbles: true,
                            cancelable: true
                        });
                        textArea.dispatchEvent(keydownEvent);
                        
                        // KeyPress (deprecated mas alguns sites ainda usam)
                        const keypressEvent = new KeyboardEvent('keypress', {
                            key: char,
                            code: 'Key' + char.toUpperCase(),
                            keyCode: char.charCodeAt(0),
                            which: char.charCodeAt(0),
                            bubbles: true,
                            cancelable: true
                        });
                        textArea.dispatchEvent(keypressEvent);
                        
                        // Adiciona o caractere
                        textArea.value += char;
                        
                        // Input event
                        const inputEvent = new InputEvent('input', {
                            data: char,
                            inputType: 'insertText',
                            bubbles: true,
                            cancelable: true
                        });
                        textArea.dispatchEvent(inputEvent);
                        
                        // KeyUp
                        const keyupEvent = new KeyboardEvent('keyup', {
                            key: char,
                            code: 'Key' + char.toUpperCase(),
                            keyCode: char.charCodeAt(0),
                            which: char.charCodeAt(0),
                            bubbles: true,
                            cancelable: true
                        });
                        textArea.dispatchEvent(keyupEvent);
                    }
                    
                    // Dispara evento de change final
                    textArea.dispatchEvent(new Event('change', { bubbles: true }));
                }
            }, 100);
            
            // Método 3: Tenta forçar uma atualização do React se os métodos anteriores falharem
            setTimeout(() => {
                if (textArea.value === AUTO_CONTINUE_TEXT) {
                    console.log("✍️ Texto 'Continue' adicionado com sucesso!");
                    
                    // Simula um espaço e backspace para forçar reconhecimento
                    const spaceDown = new KeyboardEvent('keydown', {
                        key: ' ',
                        code: 'Space',
                        keyCode: 32,
                        which: 32,
                        bubbles: true
                    });
                    const spaceUp = new KeyboardEvent('keyup', {
                        key: ' ',
                        code: 'Space',
                        keyCode: 32,
                        which: 32,
                        bubbles: true
                    });
                    const backspaceDown = new KeyboardEvent('keydown', {
                        key: 'Backspace',
                        code: 'Backspace',
                        keyCode: 8,
                        which: 8,
                        bubbles: true
                    });
                    const backspaceUp = new KeyboardEvent('keyup', {
                        key: 'Backspace',
                        code: 'Backspace',
                        keyCode: 8,
                        which: 8,
                        bubbles: true
                    });
                    
                    // Adiciona e remove um espaço
                    textArea.dispatchEvent(spaceDown);
                    textArea.value += ' ';
                    textArea.dispatchEvent(new InputEvent('input', {
                        data: ' ',
                        inputType: 'insertText',
                        bubbles: true
                    }));
                    textArea.dispatchEvent(spaceUp);
                    
                    setTimeout(() => {
                        textArea.dispatchEvent(backspaceDown);
                        textArea.value = textArea.value.slice(0, -1);
                        textArea.dispatchEvent(new InputEvent('input', {
                            inputType: 'deleteContentBackward',
                            bubbles: true
                        }));
                        textArea.dispatchEvent(backspaceUp);
                    }, 50);
                }
            }, 500);
            
        } else {
            console.warn("⚠️ Caixa de texto não encontrada");
        }
    }

    /**
     * Verifica se um elemento está visível e interativo
     */
    function isElementClickable(element) {
        if (!element) return false;
        
        // Verifica se o elemento está no DOM
        if (!document.body.contains(element)) return false;
        
        // Verifica se o elemento está visível
        const style = window.getComputedStyle(element);
        if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
        
        // Verifica se o elemento não está desabilitado
        if (element.disabled) return false;
        
        // Verifica se o elemento tem dimensões
        const rect = element.getBoundingClientRect();
        if (rect.width === 0 || rect.height === 0) return false;
        
        // Verifica se não há overlay sobre o elemento
        const elementAtPoint = document.elementFromPoint(rect.left + rect.width/2, rect.top + rect.height/2);
        if (!elementAtPoint || (!element.contains(elementAtPoint) && !elementAtPoint.contains(element))) return false;
        
        return true;
    }

    /**
     * Encontra o botão "Not now" no DOM
     */
    function findNotNowButton() {
        // Primeiro tenta encontrar pelo container específico
        const container = document.querySelector('div.rt-Flex.rt-r-fd-column.rt-r-ai-center');
        if (container) {
            const buttons = container.querySelectorAll('button[data-accent-color="gray"].rt-Button');
            for (const button of buttons) {
                if (button.textContent && button.textContent.trim() === 'Not now') {
                    return button;
                }
            }
        }
        
        // Se não encontrar, tenta a busca geral
        const allButtons = document.querySelectorAll('button[data-accent-color="gray"].rt-Button');
        for (const button of allButtons) {
            if (button.textContent && button.textContent.trim() === 'Not now') {
                return button;
            }
        }
        
        return null;
    }

    /**
     * Monitora e clica no botão "Not now" quando ele estiver disponível
     */
    function monitorAndClickNotNow() {
        // Cancela o monitor anterior se existir
        if (currentNotNowMonitor) {
            clearInterval(currentNotNowMonitor);
            console.log("🔄 Cancelando monitor anterior do botão 'Not now'");
        }
        
        console.log("🔍 Iniciando novo monitoramento do botão 'Not now'...");
        
        let elapsedTime = 0;
        let buttonFoundOnce = false;
        
        currentNotNowMonitor = setInterval(() => {
            const notNowButton = findNotNowButton();
            
            if (notNowButton) {
                if (!buttonFoundOnce) {
                    buttonFoundOnce = true;
                    console.log("👀 Botão 'Not now' detectado no DOM");
                }
                
                if (isElementClickable(notNowButton)) {
                    console.log("✅ Botão 'Not now' está clicável!");
                    notNowButton.click();
                    console.log("🖱️ Botão 'Not now' clicado automaticamente");
                    clearInterval(currentNotNowMonitor);
                    currentNotNowMonitor = null;
                } else {
                    console.log(`⌛ Botão 'Not now' ainda não está clicável, aguardando... (${elapsedTime/1000}s)`);
                }
            } else if (buttonFoundOnce) {
                // Se o botão foi encontrado antes mas agora desapareceu, para o monitor
                console.log("❌ Botão 'Not now' desapareceu do DOM");
                clearInterval(currentNotNowMonitor);
                currentNotNowMonitor = null;
            }
            
            elapsedTime += NOT_NOW_CHECK_INTERVAL_MS;
            
            // Timeout após o tempo máximo de espera
            if (elapsedTime >= NOT_NOW_MAX_WAIT_MS) {
                console.log("⏱️ Tempo limite excedido. Parando monitoramento do botão 'Not now'");
                clearInterval(currentNotNowMonitor);
                currentNotNowMonitor = null;
            }
        }, NOT_NOW_CHECK_INTERVAL_MS);
    }

    /**
     * Verifica qual tipo de botão está visível na interface.
     * @returns {'stop' | 'send-enabled' | 'send-disabled' | 'none'} O estado atual do botão.
     */
    function getButtonType() {
        // Procura pelo botão de parar (stop)
        const stopButton = document.querySelector('button:has(svg[data-icon="stop"])');
        if (stopButton) {
            return 'stop';
        }

        // Procura pelo botão de enviar (paper-plane)
        const sendButton = document.querySelector('button:has(svg[data-icon="paper-plane-top"])');
        if (sendButton) {
            // Verifica se o botão de enviar está desabilitado
            return sendButton.disabled ? 'send-disabled' : 'send-enabled';
        }

        return 'none';
    }

    /**
     * Função principal que monitora a mudança de estado e decide quando tocar o som.
     */
    function monitorStateChange() {
        const currentState = getButtonType();

        // Se o estado não foi inicializado ainda, apenas define o estado inicial.
        if (lastState === null) {
            lastState = currentState;
            return;
        }

        // Se o estado mudou, processa a lógica.
        if (currentState !== lastState) {
            console.log(`Mudança de estado detectada: de '${lastState}' para '${currentState}'`);

            // SEMPRE procura o botão "Not now" quando há qualquer mudança de estado
            console.log("🔍 Mudança de estado detectada, verificando presença do botão 'Not now'...");
            monitorAndClickNotNow();

            // CONDIÇÃO ORIGINAL:
            // Se o estado anterior era 'stop' e o novo estado é qualquer tipo de 'send',
            // significa que a geração da resposta acabou de terminar.
            if (lastState === 'stop' && currentState.startsWith('send')) {
                console.log("✅ Resposta completa!");
                playSound();
                
                // Adiciona um delay maior para garantir que a interface esteja pronta
                setTimeout(() => {
                    addContinueText();
                }, 300);
            }

            // Atualiza o último estado conhecido.
            lastState = currentState;
        }
    }

    // Aguarda um momento para garantir que a interface do site foi carregada
    // antes de iniciar o monitoramento.
    setTimeout(() => {
        // Inicializa o estado pela primeira vez.
        lastState = getButtonType();
        console.log(`Estado inicial do botão: '${lastState}'`);

        // Inicia o monitoramento contínuo (polling).
        setInterval(monitorStateChange, POLLING_INTERVAL_MS);

        console.log("✅ Script iniciado com sucesso! Monitorando o botão de resposta.");
        console.log("ℹ️ O som tocará e 'Continue' será adicionado quando a resposta do modelo terminar de ser gerada.");
        console.log("ℹ️ O botão 'Not now' será verificado e clicado automaticamente a cada mudança de estado.");
    }, 1500);

    // --- FUNÇÕES DE DEBUG (Opcional) ---
    function debugElements() {
        console.log("=== INFORMAÇÕES DE DEBUG ===");
        const sendButton = document.querySelector('button:has(svg[data-icon="paper-plane-top"])');
        const stopButton = document.querySelector('button:has(svg[data-icon="stop"])');
        const textArea = document.querySelector('textarea.ChatInput_textarea__QUOCH');
        const notNowButton = findNotNowButton();
        const container = document.querySelector('div.rt-Flex.rt-r-fd-column.rt-r-ai-center');
        
        console.log("Botão de Enviar (Send) encontrado:", sendButton);
        if (sendButton) {
            console.log("HTML do Botão de Enviar:", sendButton.outerHTML);
            console.log("Botão Send está desabilitado?", sendButton.disabled);
        }
        console.log("Botão de Parar (Stop) encontrado:", stopButton);
        if (stopButton) console.log("HTML do Botão de Parar:", stopButton.outerHTML);
        console.log("Caixa de texto encontrada:", textArea);
        if (textArea) {
            console.log("HTML da Caixa de texto:", textArea.outerHTML);
            console.log("Valor atual da textarea:", textArea.value);
        }
        
        console.log("Botão 'Not now' encontrado:", notNowButton);
        if (notNowButton) {
            console.log("HTML do Botão 'Not now':", notNowButton.outerHTML);
            console.log("Botão 'Not now' clicável?", isElementClickable(notNowButton));
        }
        
        console.log("Container dos botões encontrado:", container);
        if (container) {
            console.log("HTML do container:", container.outerHTML);
        }
        
        console.log("Estado atual (getButtonType):", getButtonType());
        console.log("Último estado registrado (lastState):", lastState);
        console.log("Monitor 'Not now' ativo?", currentNotNowMonitor !== null);
        console.log("=== FIM DO DEBUG ===");
    }

    // Adiciona função para forçar clique no Not now
    window.forceNotNowClick = function() {
        const button = findNotNowButton();
        if (button) {
            console.log("Forçando clique no botão 'Not now'");
            button.click();
            return true;
        } else {
            console.log("Botão 'Not now' não encontrado");
            return false;
        }
    };

    // Adiciona função para testar clicabilidade no console
    window.testClickable = function(selector) {
        const element = document.querySelector(selector);
        if (element) {
            console.log("Elemento encontrado:", element);
            console.log("É clicável?", isElementClickable(element));
            return isElementClickable(element);
        } else {
            console.log("Elemento não encontrado com o seletor:", selector);
            return false;
        }
    };

    // Adiciona o comando de debug à janela para que possa ser chamado pelo console.
    window.debugOutlierScript = debugElements;
    console.log("💡 Dica: Digite 'debugOutlierScript()' no console para verificar o estado dos elementos.");
    console.log("💡 Dica: Digite 'testClickable(selector)' no console para testar se um elemento é clicável.");
    console.log("💡 Dica: Digite 'forceNotNowClick()' no console para forçar o clique no botão 'Not now'.");

})();