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.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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       Sergi0
// @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');
})();