Amateur.tv Chat Reader

Lee en voz alta los mensajes del chat de Amateur.tv con volumen independiente para la voz del modelo y del usuario, selección de voces y panel plegable.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Amateur.tv Chat Reader
// @namespace    http://tampermonkey.net/
// @version      20.0
// @description  Lee en voz alta los mensajes del chat de Amateur.tv con volumen independiente para la voz del modelo y del usuario, selección de voces y panel plegable.
// @author       ChatGPT (Corregido - Versión 20.0 FINAL)
// @match        https://es.amateur.tv/*
// @match        https://www.amateur.tv/*
// @match        https://amateur.tv/*
// @grant        GM_xmlhttpRequest
// @connect      api.elevenlabs.io
// @icon         https://www.amateur.tv/favicon.ico
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    console.log('ChatReader: script started. Version 20.0 FINAL');

    // ===== CONFIGURACIÓN DE API KEY =====
    // Obtén tu API key GRATIS en: https://elevenlabs.io (20,000 caracteres/mes)
    // 1. Regístrate gratis en https://elevenlabs.io
    // 2. Ve a https://elevenlabs.io/app/developers/api-keys
    // 3. Copia tu API key y pégala aquí:
    const ELEVENLABS_API_KEY = 'YOUR_API_KEY_HERE';
    const API_KEY_PLACEHOLDERS = ['TU_API_KEY_AQUI', 'YOUR_API_KEY_HERE'];

    // ===== DETECCIÓN DEL CHAT =====
    // Estructura 2026: el chat usa la clase estable "cy_chatContainer".
    // Mantenemos los selectores antiguos como respaldo por si la web los recupera:
    const CHAT_SELECTORS = [
        '.cy_chatContainer',
        '[class*="cy_chatContainer"]',
        '[class*="ChatBlockLegacy__ChatBlockContainer"]',
        '[class*="ChatBlock__ChatBlockContainer"]',
        '[class*="ChatBlockContainer"]',
        '[class*="ChatBlockLegacy"]',
        '[class*="ChatBlock"]'
    ];

    // Voces disponibles en español (con género).
    // NOTA: Charlotte, Grace, Rachel, Domi y Elli son voces de PAGO en ElevenLabs
    // (error 402), se han sustituido por voces del plan gratuito.
    const AVAILABLE_VOICES = {
        // VOCES FEMENINAS (plan gratuito)
        'Sarah': { id: 'EXAVITQu4vr4xnSDxMaL', gender: 'female', name: 'Sarah (Femenina - Suave)' },
        'Laura': { id: 'FGY2WhTYpPnrIDTdsKH5', gender: 'female', name: 'Laura (Femenina - Joven)' },
        'Alice': { id: 'Xb7hH8MSUJpSbSDYk0k2', gender: 'female', name: 'Alice (Femenina - Natural)' },
        'Lily': { id: 'pFZP5JQG7iQjIQuC4Bku', gender: 'female', name: 'Lily (Femenina - Cálida)' },
        'Matilda': { id: 'XrExE9yKIg1WjnnlVkGX', gender: 'female', name: 'Matilda (Femenina - Joven)' },
        'Jessica': { id: 'cgSgspJ2msm6clMCkdW9', gender: 'female', name: 'Jessica (Femenina - Cercana)' },

        // VOCES MASCULINAS (todas verificadas funcionando en tu cuenta)
        'Bill': { id: 'pqHfZKP75CvOlQylNhV4', gender: 'male', name: 'Bill (Masculina - Firme)' },
        'Daniel': { id: 'onwK4e9ZLuTAKqWW03F9', gender: 'male', name: 'Daniel (Masculina - Profunda)' },
        'Callum': { id: 'N2lVS1w4EtoT3dr4eOWO', gender: 'male', name: 'Callum (Masculina - Juvenil)' },
        'George': { id: 'JBFqnCBsd6RMkjVDRZzb', gender: 'male', name: 'George (Masculina - Natural)' },
        'Eric': { id: 'cjVigY5qzO86Huf0OWal', gender: 'male', name: 'Eric (Masculina - Cálida)' }
    };

    // ========================================

    let lastReadMessageCount = 0;
    let chatObserver = null;
    let isSpeechEnabled = false;
    let controlsContainer = null;
    let enableButton = null;
    let statusDiv = null;

    let modelVolumeSlider = null;
    let userVolumeSlider = null;
    let modelVoiceSelect = null;
    let userVoiceSelect = null;

    let modelVoice = localStorage.getItem('chatReaderModelVoice') || 'Sarah';
    let userVoice = localStorage.getItem('chatReaderUserVoice') || 'Bill';

    // Si la voz guardada ya no existe en la lista (p.ej. Charlotte/Grace premium),
    // volvemos a una voz gratuita por defecto
    if (!AVAILABLE_VOICES[modelVoice]) {
        modelVoice = 'Sarah';
        localStorage.setItem('chatReaderModelVoice', modelVoice);
    }
    if (!AVAILABLE_VOICES[userVoice]) {
        userVoice = 'Bill';
        localStorage.setItem('chatReaderUserVoice', userVoice);
    }

    let urlCheckInterval = null;
    let detectionInterval = null;
    let bodyWatcher = null;
    let toggleButton = null;
    let currentChatContainer = null;
    let audioQueue = [];
    let isPlaying = false;
    let currentAudio = null;
    let pendingProcessTimer = null;

    const IS_TOP_WINDOW = (window.self === window.top);

    // Cola de reproducción
    function playNextInQueue() {
        if (audioQueue.length === 0) {
            isPlaying = false;
            return;
        }

        isPlaying = true;
        const audioData = audioQueue.shift();

        const audio = new Audio();
        currentAudio = audio;
        audio.volume = audioData.volume;

        audio.onloadeddata = () => {
            console.log('ChatReader: Audio loaded, playing...');
        };

        audio.onended = () => {
            console.log('ChatReader: Finished playing audio');
            playNextInQueue();
        };

        audio.onerror = (e) => {
            console.error('ChatReader: Audio playback error:', e);
            playNextInQueue();
        };

        audio.src = URL.createObjectURL(audioData.blob);
        audio.play().catch(err => {
            console.error('ChatReader: Error playing audio:', err);
            playNextInQueue();
        });
    }

    // TTS con ElevenLabs
    function speakWithElevenLabs(text, isModelVoice = true, forcePlay = false) {
        if (!isSpeechEnabled && !forcePlay) {
            console.log('ChatReader: Speech is disabled. Skipping:', text);
            return;
        }

        if (!ELEVENLABS_API_KEY || API_KEY_PLACEHOLDERS.indexOf(ELEVENLABS_API_KEY) !== -1) {
            console.error('ChatReader: API key not configured!');
            alert('❌ Este script requiere configuración inicial.\n\nEdita el script y pega tu clave en la constante ELEVENLABS_API_KEY (al principio del código).\n\nMás info en: https://elevenlabs.io/app/developers/api-keys');
            return;
        }

        const voiceName = isModelVoice ? modelVoice : userVoice;
        const voiceData = AVAILABLE_VOICES[voiceName];

        if (!voiceData) {
            console.error('ChatReader: Voice not found:', voiceName);
            return;
        }

        const volume = isModelVoice ?
            (modelVolumeSlider ? parseFloat(modelVolumeSlider.value) : 1.0) :
            (userVolumeSlider ? parseFloat(userVolumeSlider.value) : 1.0);

        const url = `https://api.elevenlabs.io/v1/text-to-speech/${voiceData.id}`;

        const requestData = {
            text: text,
            model_id: 'eleven_multilingual_v2',
            voice_settings: {
                stability: 0.5,
                similarity_boost: 0.75,
                style: 0.0,
                use_speaker_boost: true
            }
        };

        console.log(`ChatReader: Requesting TTS for: "${text.substring(0, 30)}..." with voice: ${voiceData.name}`);

        GM_xmlhttpRequest({
            method: 'POST',
            url: url,
            headers: {
                'Accept': 'audio/mpeg',
                'Content-Type': 'application/json',
                'xi-api-key': ELEVENLABS_API_KEY
            },
            data: JSON.stringify(requestData),
            responseType: 'blob',
            onload: function(response) {
                if (response.status === 200) {
                    console.log('ChatReader: Audio generated successfully');
                    audioQueue.push({
                        blob: response.response,
                        volume: volume
                    });

                    if (!isPlaying) {
                        playNextInQueue();
                    }
                } else if (response.status === 401) {
                    console.error('ChatReader: Invalid API key');
                    alert('❌ Clave inválida. Verifica la configuración del script.');
                } else if (response.status === 402) {
                    console.error('ChatReader: 402 con la voz:', voiceData.name);
                    if (response.response && response.response.text) {
                        response.response.text().then(t => console.error('ChatReader: detalle API:', t)).catch(() => {});
                    }
                    alert('⚠️ ElevenLabs rechazó la voz "' + voiceData.name + '" (402).\n\nEsta voz requiere plan de pago, o se acabó tu cuota del mes.\n\n• Prueba otra voz con "🔊 Probar Voz".\n• Saldo y consumo: https://elevenlabs.io/app/usage');
                } else if (response.status === 429) {
                    console.error('ChatReader: Rate limit exceeded');
                    alert('⚠️ Has excedido el límite de uso gratuito.\n\nEspera hasta el próximo mes o actualiza tu plan.');
                } else {
                    console.error('ChatReader: API error:', response.status, response.statusText);
                    alert(`❌ Error: ${response.status}\n\nRevisa la consola para más detalles.`);
                }
            },
            onerror: function(error) {
                console.error('ChatReader: Request error:', error);
                alert('❌ Error de conexión. Verifica tu internet y la configuración de Tampermonkey.');
            }
        });
    }

    // Inject CSS
    function injectStyles() {
        if (document.getElementById('chatReaderStyles')) return; // Ya existe

        const style = document.createElement('style');
        style.id = 'chatReaderStyles';
        style.type = 'text/css';
        style.innerHTML = `
            #chatReaderPanel {
                position: fixed;
                top: 50%;
                right: -290px;
                transform: translateY(-50%);
                width: 270px;
                box-sizing: border-box;
                background-color: rgba(0, 0, 0, 0.9);
                border-radius: 8px 0 0 8px;
                box-shadow: 0 4px 15px rgba(0,0,0,0.5);
                padding: 15px;
                display: flex;
                flex-direction: column;
                gap: 10px;
                font-family: Arial, sans-serif;
                color: white;
                transition: right 0.3s ease-in-out;
                z-index: 99999;
            }

            #chatReaderPanel.open {
                right: 0;
            }

            #chatReaderToggleButton {
                position: fixed;
                top: 50%;
                right: 0;
                transform: translateY(-50%) rotate(270deg);
                transform-origin: bottom right;
                background-color: rgba(0, 0, 0, 0.9);
                color: white;
                padding: 8px 12px;
                border: none;
                border-radius: 8px 8px 0 0;
                cursor: pointer;
                font-size: 13px;
                z-index: 100000;
                box-shadow: 0 4px 15px rgba(0,0,0,0.5);
                transition: right 0.3s ease-in-out;
            }

            #chatReaderPanel.open + #chatReaderToggleButton {
                right: 270px;
            }

            #chatReaderPanel button {
                padding: 8px 12px;
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                font-size: 13px;
            }

            #chatReaderPanel select {
                width: 100%;
                padding: 6px;
                border-radius: 3px;
                border: 1px solid #555;
                background-color: #222;
                color: white;
                font-size: 12px;
                cursor: pointer;
            }

            #chatReaderPanel select option {
                background-color: #222;
                color: white;
            }

            #chatReaderPanel .volume-control-wrapper {
                display: flex;
                flex-direction: column;
                gap: 5px;
                font-size: 12px;
            }

            #chatReaderPanel #enableButton.enabled {
                background-color: #28a745;
            }

            #chatReaderPanel #enableButton.disabled {
                background-color: #dc3545;
            }

            #chatReaderPanel input[type="range"] {
                width: 100%;
                cursor: pointer;
            }

            #chatReaderPanel .test-button {
                margin-top: 5px;
                padding: 6px 10px;
                background-color: #007bff;
                font-size: 11px;
            }

            #chatReaderPanel .test-button:hover {
                background-color: #0056b3;
            }

            #chatReaderPanel .status {
                font-size: 11px;
                padding: 6px 8px;
                border-radius: 4px;
                background: rgba(255, 255, 255, 0.08);
                color: #cccccc;
                text-align: center;
            }

            #chatReaderPanel .status.ok {
                color: #4CAF50;
                background: rgba(76, 175, 80, 0.15);
            }

            #chatReaderPanel .voice-section {
                display: flex;
                flex-direction: column;
                gap: 5px;
                padding: 10px;
                background: rgba(255, 255, 255, 0.05);
                border-radius: 5px;
                border: 1px solid rgba(255, 255, 255, 0.1);
            }

            #chatReaderPanel .voice-section label {
                font-size: 11px;
                color: #4CAF50;
                font-weight: bold;
                margin-bottom: 3px;
            }
        `;
        document.head.appendChild(style);
    }

    function updateToggleButtonState() {
        if (enableButton) {
            if (isSpeechEnabled) {
                enableButton.textContent = 'Deshabilitar Lectura';
                enableButton.classList.remove('disabled');
                enableButton.classList.add('enabled');
            } else {
                enableButton.textContent = 'Habilitar Lectura';
                enableButton.classList.remove('enabled');
                enableButton.classList.add('disabled');
            }
        }
        updateStatus();
    }

    function updateStatus() {
        if (!statusDiv) return;
        const chatState = currentChatContainer ? '✅ Chat detectado' : '⏳ Buscando chat… (entra en un directo)';
        const speechState = isSpeechEnabled ? '🔊 Lectura ON' : '🔇 Lectura OFF';
        statusDiv.textContent = chatState + ' · ' + speechState;
        statusDiv.className = 'status' + (currentChatContainer ? ' ok' : '');
    }

    function createControls() {
        // EVITAR CREAR MÚLTIPLES VECES
        if (controlsContainer && document.body.contains(controlsContainer)) {
            updateToggleButtonState();
            return;
        }

        injectStyles();

        controlsContainer = document.createElement('div');
        controlsContainer.id = 'chatReaderPanel';

        statusDiv = document.createElement('div');
        statusDiv.className = 'status';

        enableButton = document.createElement('button');
        enableButton.id = 'enableButton';
        enableButton.onclick = () => {
            isSpeechEnabled = !isSpeechEnabled;
            updateToggleButtonState();
            if (isSpeechEnabled) {
                console.log('ChatReader: Speech enabled.');
                if (currentChatContainer) {
                    startChatMessagesObserver(currentChatContainer);
                    setTimeout(() => processNewMessages(currentChatContainer, true), 100);
                }
                handleChatContainer(); // Por si el chat acaba de aparecer
            } else {
                audioQueue = [];
                if (currentAudio) {
                    currentAudio.pause();
                    currentAudio = null;
                }
                disconnectChatObserver();
                console.log('ChatReader: Speech disabled.');
            }
        };
        updateToggleButtonState();

        // Sección Modelo (Femenina)
        const modelSection = document.createElement('div');
        modelSection.className = 'voice-section';

        const modelLabel = document.createElement('label');
        modelLabel.textContent = '👩 VOZ MODELO (FEMENINA)';

        modelVoiceSelect = document.createElement('select');
        Object.entries(AVAILABLE_VOICES).forEach(([key, voice]) => {
            if (voice.gender === 'female') {
                const option = document.createElement('option');
                option.value = key;
                option.textContent = voice.name;
                if (key === modelVoice) option.selected = true;
                modelVoiceSelect.appendChild(option);
            }
        });
        modelVoiceSelect.onchange = (e) => {
            modelVoice = e.target.value;
            localStorage.setItem('chatReaderModelVoice', modelVoice);
            console.log('Model voice changed to:', modelVoice);
        };

        const modelVolWrapper = document.createElement('div');
        modelVolWrapper.className = 'volume-control-wrapper';
        const modelVolLabel = document.createElement('span');
        const initialModelVol = parseFloat(localStorage.getItem('chatReaderModelVolume') || '1');
        modelVolLabel.textContent = `Volumen: ${Math.round(initialModelVol * 100)}%`;
        modelVolumeSlider = document.createElement('input');
        modelVolumeSlider.type = 'range';
        modelVolumeSlider.min = '0';
        modelVolumeSlider.max = '1';
        modelVolumeSlider.step = '0.05';
        modelVolumeSlider.value = initialModelVol;
        modelVolumeSlider.oninput = (e) => {
            const vol = parseFloat(e.target.value);
            modelVolLabel.textContent = `Volumen: ${Math.round(vol * 100)}%`;
            localStorage.setItem('chatReaderModelVolume', vol);
        };
        modelVolWrapper.appendChild(modelVolLabel);
        modelVolWrapper.appendChild(modelVolumeSlider);

        const modelTestBtn = document.createElement('button');
        modelTestBtn.className = 'test-button';
        modelTestBtn.textContent = '🔊 Probar Voz';
        modelTestBtn.onclick = () => {
            speakWithElevenLabs('Hola, soy la voz del modelo', true, true);
        };

        modelSection.appendChild(modelLabel);
        modelSection.appendChild(modelVoiceSelect);
        modelSection.appendChild(modelVolWrapper);
        modelSection.appendChild(modelTestBtn);

        // Sección Usuario (Masculina)
        const userSection = document.createElement('div');
        userSection.className = 'voice-section';

        const userLabel = document.createElement('label');
        userLabel.textContent = '👨 VOZ USUARIO (MASCULINA)';

        userVoiceSelect = document.createElement('select');
        Object.entries(AVAILABLE_VOICES).forEach(([key, voice]) => {
            if (voice.gender === 'male') {
                const option = document.createElement('option');
                option.value = key;
                option.textContent = voice.name;
                if (key === userVoice) option.selected = true;
                userVoiceSelect.appendChild(option);
            }
        });
        userVoiceSelect.onchange = (e) => {
            userVoice = e.target.value;
            localStorage.setItem('chatReaderUserVoice', userVoice);
            console.log('User voice changed to:', userVoice);
        };

        const userVolWrapper = document.createElement('div');
        userVolWrapper.className = 'volume-control-wrapper';
        const userVolLabel = document.createElement('span');
        const initialUserVol = parseFloat(localStorage.getItem('chatReaderUserVolume') || '1');
        userVolLabel.textContent = `Volumen: ${Math.round(initialUserVol * 100)}%`;
        userVolumeSlider = document.createElement('input');
        userVolumeSlider.type = 'range';
        userVolumeSlider.min = '0';
        userVolumeSlider.max = '1';
        userVolumeSlider.step = '0.05';
        userVolumeSlider.value = initialUserVol;
        userVolumeSlider.oninput = (e) => {
            const vol = parseFloat(e.target.value);
            userVolLabel.textContent = `Volumen: ${Math.round(vol * 100)}%`;
            localStorage.setItem('chatReaderUserVolume', vol);
        };
        userVolWrapper.appendChild(userVolLabel);
        userVolWrapper.appendChild(userVolumeSlider);

        const userTestBtn = document.createElement('button');
        userTestBtn.className = 'test-button';
        userTestBtn.textContent = '🔊 Probar Voz';
        userTestBtn.onclick = () => {
            speakWithElevenLabs('Hola, soy la voz del usuario', false, true);
        };

        userSection.appendChild(userLabel);
        userSection.appendChild(userVoiceSelect);
        userSection.appendChild(userVolWrapper);
        userSection.appendChild(userTestBtn);

        controlsContainer.appendChild(enableButton);
        controlsContainer.appendChild(statusDiv);
        controlsContainer.appendChild(modelSection);
        controlsContainer.appendChild(userSection);

        document.body.appendChild(controlsContainer);

        toggleButton = document.createElement('button');
        toggleButton.id = 'chatReaderToggleButton';
        toggleButton.textContent = 'Chat Reader';
        toggleButton.onclick = () => {
            controlsContainer.classList.toggle('open');
        };
        document.body.appendChild(toggleButton);

        updateStatus();
        console.log('ChatReader: Panel de control creado correctamente');
    }

    function getMessageWrappers(chatContainer) {
        // Estructura 2026: filas "py-px" con autor + spans de texto.
        // En los mensajes de usuario el <button> es hijo directo de la fila; en los
        // de la MODELO va dentro de la insignia (span > button). Por eso buscamos
        // el botón en cualquier nivel de la fila.
        // SIN fallbacks genéricos a propósito: las filas de sistema y avisos
        // ("X ha comenzado a seguir esta cam", menús de juegos, "Chat conectado"...)
        // usan otras clases (my-[2px] etc.) y así NUNCA se leen por error.
        return Array.from(chatContainer.querySelectorAll('div')).filter(el => {
            const cls = (typeof el.className === 'string') ? el.className : '';
            return cls.indexOf('py-px') !== -1 &&
                   el.querySelector('button') &&
                   el.querySelector(':scope > span');
        });
    }

    function extractMessage(wrapper) {
        // 1) Estructura antigua (por si alguna sección la conserva)
        const oldText = wrapper.querySelector('span.sc-eqUAAy');
        const oldAuthor = wrapper.querySelector('span.sc-gEvEer');

        if (oldText && oldAuthor) {
            const oldMsgText = oldText.textContent.trim();
            if (oldMsgText) {
                return {
                    text: oldMsgText,
                    isModel: oldAuthor.querySelector('svg') !== null
                };
            }
        }

        // 2) Estructura nueva (2026): BUTTON = autor, SPANs directos = texto
        const authorButton = wrapper.querySelector(':scope > button');
        let spans = Array.from(wrapper.querySelectorAll(':scope > span'))
            .filter(s => s.textContent.trim().length > 0 &&
                         !s.querySelector('svg') && !s.querySelector('img'));

        // 3) Fallback genérico: el span con más texto sin iconos
        if (spans.length === 0) {
            spans = Array.from(wrapper.querySelectorAll('span'))
                .filter(s => s.textContent.trim().length > 0 &&
                             !s.querySelector('svg') && !s.querySelector('img'));
        }

        if (spans.length === 0) return null;

        spans.sort((a, b) => b.textContent.trim().length - a.textContent.trim().length);
        const text = spans[0].textContent.trim();
        if (!text) return null;

        // ¿Mensaje de la MODELO?
        // Modelo: el botón del autor va dentro de una insignia con
        // background-color: var(--color-transmitter-badge) (icono de cámara),
        // el svg es viewBox 24x24 (cámara) y el texto usa color-chat-transmitter.
        // Un usuario destacado también va en negrita pero SIN insignia de cámara
        // (su svg es distinto, p.ej. viewBox 16x19).
        let isModel = false;

        const badgeSpan = wrapper.querySelector('span[style*="transmitter-badge"]');
        if (badgeSpan) {
            isModel = true; // insignia de cámara de la modelo
        }

        if (wrapper.querySelector('svg[viewBox="0 0 24 24"]')) {
            isModel = true;
        }
        if (wrapper.querySelector('span[style*="color-chat-transmitter"]')) {
            isModel = true;
        }

        // En la estructura antigua decidíamos por el svg del autor
        if (!badgeSpan && oldAuthor && oldAuthor.querySelector('svg')) {
            isModel = true;
        }

        return { text: text, isModel: isModel };
    }

    function processNewMessages(chatContainer, isInitialLoad = false) {
        if (!isSpeechEnabled && !isInitialLoad) return;
        if (!chatContainer || chatContainer !== currentChatContainer) return;

        const messages = getMessageWrappers(chatContainer);

        if (messages.length === 0) {
            lastReadMessageCount = 0;
            return;
        }

        // Si la web recorta mensajes antiguos (la lista tiene tope), reajustamos
        if (messages.length < lastReadMessageCount) {
            lastReadMessageCount = messages.length;
        }

        let startIndex = lastReadMessageCount;

        if (isInitialLoad) {
            // Al entrar, solo leer el último mensaje existente
            startIndex = Math.max(0, messages.length - 1);
        }

        if (messages.length > lastReadMessageCount) {
            for (let i = startIndex; i < messages.length; i++) {
                const info = extractMessage(messages[i]);

                if (info && info.text) {
                    console.log(`ChatReader: Processing ${info.isModel ? 'model' : 'user'} message:`, info.text);
                    speakWithElevenLabs(info.text, info.isModel, false);
                }
            }
        }

        lastReadMessageCount = messages.length;
    }

    function stopCurrentAudio() {
        if (currentAudio) {
            currentAudio.pause();
            currentAudio = null;
        }
    }

    function hideControls() {
        if (controlsContainer && document.body.contains(controlsContainer)) {
            document.body.removeChild(controlsContainer);
        }
        if (toggleButton && document.body.contains(toggleButton)) {
            document.body.removeChild(toggleButton);
        }
        // Al ocultarse, el panel debe reaparecer CERRADO la próxima vez
        if (controlsContainer) {
            controlsContainer.classList.remove('open');
        }
        if (isSpeechEnabled) {
            isSpeechEnabled = false;
            updateToggleButtonState();
            console.log('ChatReader: Lectura desactivada (salimos del chat de la modelo)');
        }
        audioQueue = [];
        stopCurrentAudio();
    }

    function ensureControls() {
        if (!IS_TOP_WINDOW) return;

        // El panel SOLO se muestra en páginas de modelo (donde hay chat).
        // Si salimos a otra parte de la web, se cierra y desactiva la lectura:
        if (!currentChatContainer) {
            hideControls();
            return;
        }

        // La app (React) limpia el <body> al montar y borra nuestro panel:
        // lo volvemos a pegar / crear automáticamente
        if (controlsContainer && !document.body.contains(controlsContainer)) {
            document.body.appendChild(controlsContainer); // reutilizar el existente
        }
        if (toggleButton && !document.body.contains(toggleButton)) {
            document.body.appendChild(toggleButton);
        }
        if (!controlsContainer) {
            if (toggleButton && document.body.contains(toggleButton)) {
                toggleButton.remove();
            }
            createControls();
        }
    }

    function findChatContainer() {
        for (let i = 0; i < CHAT_SELECTORS.length; i++) {
            const sel = CHAT_SELECTORS[i];
            try {
                const el = document.querySelector(sel);
                if (el) {
                    return el;
                }
            } catch (e) {
                // Selector inválido, continuar con el siguiente
            }
        }
        return null;
    }

    function disconnectChatObserver() {
        if (chatObserver) {
            chatObserver.disconnect();
            chatObserver = null;
        }
    }

    function resetChatState() {
        currentChatContainer = null;
        disconnectChatObserver();
        lastReadMessageCount = 0;
        updateStatus();
    }

    function handleChatContainer() {
        const foundChatContainer = findChatContainer();

        if (foundChatContainer) {
            // Mostrar/recrear el panel SOLO cuando hay chat (página de modelo)
            ensureControls();

            if (!controlsContainer) {
                createControls(); // caso iframe: el chat vive dentro del iframe
            }

            if (foundChatContainer !== currentChatContainer) {
                console.log('ChatReader: Chat container encontrado');
                // Al cambiar de modelo, el panel reaparece CERRADO
                if (controlsContainer) {
                    controlsContainer.classList.remove('open');
                }
                currentChatContainer = foundChatContainer;
                lastReadMessageCount = 0;
                disconnectChatObserver();
                if (isSpeechEnabled) {
                    startChatMessagesObserver(foundChatContainer);
                    setTimeout(() => processNewMessages(foundChatContainer, true), 100);
                }
                updateStatus();
            }
        } else {
            if (currentChatContainer) {
                console.log('ChatReader: Salimos de la modelo, desactivando y cerrando');
                resetChatState();
            }
            // Sin chat → ocultar panel y desactivar lectura
            ensureControls();
        }
    }

    function startChatMessagesObserver(chatContainer) {
        if (!chatContainer) return;
        disconnectChatObserver();

        chatObserver = new MutationObserver(mutations => {
            if (!isSpeechEnabled || !currentChatContainer) return;
            const hasAddedNodes = mutations.some(mutation => mutation.addedNodes.length > 0);
            if (!hasAddedNodes) return;

            clearTimeout(pendingProcessTimer);
            pendingProcessTimer = setTimeout(() => processNewMessages(currentChatContainer, false), 300);
        });

        // subtree: true para no depender de dónde cuelguen exactamente los mensajes
        chatObserver.observe(chatContainer, { childList: true, subtree: true });
        console.log('ChatReader: Observer de mensajes iniciado');
    }

    // Main execution
    console.log('ChatReader: Iniciando script...');

    function init() {
        if (IS_TOP_WINDOW) {
            // El panel solo se mostrará en páginas de modelo (con chat).
            // Vigilamos el body para repagarlo si la app lo borra (React):
            bodyWatcher = new MutationObserver(() => {
                if (currentChatContainer &&
                    (!controlsContainer || !document.body.contains(controlsContainer))) {
                    ensureControls();
                }
            });
            bodyWatcher.observe(document.body, { childList: true, subtree: false });
        }
        handleChatContainer();
        detectionInterval = setInterval(handleChatContainer, 1500);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    let lastUrl = location.href;
    urlCheckInterval = setInterval(() => {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            resetChatState();
            setTimeout(handleChatContainer, 500);
        }
    }, 500);

    window.addEventListener('beforeunload', () => {
        disconnectChatObserver();
        if (bodyWatcher) bodyWatcher.disconnect();
        if (urlCheckInterval) clearInterval(urlCheckInterval);
        if (detectionInterval) clearInterval(detectionInterval);
        clearTimeout(pendingProcessTimer);
    });

    console.log('ChatReader: Script inicializado correctamente');
})();