SullyTavern Auto Resend on Empty AI

Automatically clicks the send button in Sully Tavern when "Google AI Studio Candidate text empty" error occurs, and counts the auto-resends. Resets counter on manual send (click or Enter).

// ==UserScript==
// @name         SullyTavern Auto Resend on Empty AI
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Automatically clicks the send button in Sully Tavern when "Google AI Studio Candidate text empty" error occurs, and counts the auto-resends. Resets counter on manual send (click or Enter).
// @author       Your Helper
// @match        http://127.0.0.1:8000/*
// @match        http://localhost:8000/*
// @match        http://127.0.0.1:8000
// @match        http://localhost:8000
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // --- КОНФИГУРАЦИЯ ---
    const TARGET_ERROR_SUBSTRING = "Google AI Studio Candidate text empty";
    const BUTTON_ID = "send_but";
    const TEXTAREA_ID = "send_textarea"; // <-- НОВОЕ: ID поля для ввода текста
    const SCRIPT_PREFIX = "[SullyTavern AutoResend] ";
    const COUNTER_ID = "st-autoresend-counter";

    // --- СОСТОЯНИЕ СКРИПТА ---
    let scriptInitialized = false;
    let autoClickCount = 0;
    let counterElement = null;


    // --- НОВЫЕ ФУНКЦИИ ДЛЯ СЧЕТЧИКА ---

    /**
     * Создает и стилизует элемент счетчика на странице.
     */
    function createCounterUI() {
        if (document.getElementById(COUNTER_ID)) return;

        counterElement = document.createElement('div');
        counterElement.id = COUNTER_ID;
        Object.assign(counterElement.style, {
            position: 'fixed',
            bottom: '50px',
            left: '25px',
            color: '#888888',
            fontSize: '14px',
            fontWeight: 'bold',
            zIndex: '99999',
            pointerEvents: 'none',
            display: 'none'
        });
        document.body.appendChild(counterElement);
    }

    /**
     * Обновляет отображение счетчика. Показывает его, если > 0, иначе скрывает.
     */
    function updateCounterDisplay() {
        if (!counterElement) return;

        counterElement.textContent = autoClickCount;
        counterElement.style.display = autoClickCount > 0 ? 'block' : 'none';
    }

    /**
     * Устанавливает слушатель на кнопку отправки для сброса счетчика при ручном клике.
     * @param {HTMLElement} button - Элемент кнопки отправки.
     */
    function setupManualClickReset(button) {
        if (button.dataset.manualResetListener) return;

        button.addEventListener('click', (event) => {
            if (event.isTrusted && autoClickCount > 0) {
                console.log(SCRIPT_PREFIX + "Manual send detected (click). Resetting counter.");
                autoClickCount = 0;
                updateCounterDisplay();
            }
        });
        button.dataset.manualResetListener = 'true';
    }

    /**
     * НОВАЯ ФУНКЦИЯ
     * Устанавливает слушатель на поле ввода текста для сброса счетчика при нажатии Enter.
     * @param {HTMLTextAreaElement} textarea - Элемент поля ввода.
     */
    function setupEnterKeyReset(textarea) {
        if (textarea.dataset.enterResetListener) return; // Предотвращаем повторное назначение

        textarea.addEventListener('keydown', (event) => {
            // Сбрасываем, только если нажат Enter без Shift (для отправки) и счетчик > 0
            if (event.key === 'Enter' && !event.shiftKey && autoClickCount > 0) {
                console.log(SCRIPT_PREFIX + "Manual send detected (Enter key). Resetting counter.");
                autoClickCount = 0;
                updateCounterDisplay();
            }
        });
        textarea.dataset.enterResetListener = 'true';
    }


    // --- ОСНОВНАЯ ЛОГИКА ---

    function handleError(errorSource, errorDetails) {
        let errorMessage = "";

        if (typeof errorDetails === 'string') {
            errorMessage = errorDetails;
        } else if (errorDetails instanceof Error) {
            errorMessage = errorDetails.message || "";
        } else if (errorDetails && typeof errorDetails.reason !== 'undefined') {
            if (errorDetails.reason instanceof Error) {
                errorMessage = errorDetails.reason.message || "";
            } else if (typeof errorDetails.reason === 'string') {
                errorMessage = errorDetails.reason;
            }
        }

        if (typeof errorMessage === 'string' && errorMessage.includes(TARGET_ERROR_SUBSTRING)) {
            console.log(SCRIPT_PREFIX + `Target error detected via ${errorSource}: "${errorMessage}". Clicking send button.`);
            clickSendButton();
        }
    }

    function initializeScript() {
        if (scriptInitialized) {
            return;
        }

        createCounterUI();

        if (typeof console !== 'undefined' && typeof console.error !== 'undefined') {
            const originalConsoleError = console.error;
            console.error = function(...args) {
                originalConsoleError.apply(console, args);
                if (args.length > 0) {
                    handleError("console.error", args[0]);
                }
            };
        }

        window.addEventListener('unhandledrejection', function(event) {
            handleError("unhandledrejection", event);
        });

        // ИЗМЕНЕНО: MutationObserver теперь ищет и кнопку, и поле ввода
        const observer = new MutationObserver((mutations, obs) => {
            const sendButton = document.getElementById(BUTTON_ID);
            const textarea = document.getElementById(TEXTAREA_ID);

            let allElementsFound = true;

            if (sendButton && !sendButton.dataset.manualResetListener) {
                setupManualClickReset(sendButton);
                console.log(SCRIPT_PREFIX + "Manual click reset listener attached to the send button.");
            } else if (!sendButton) {
                allElementsFound = false;
            }

            if (textarea && !textarea.dataset.enterResetListener) {
                setupEnterKeyReset(textarea);
                 console.log(SCRIPT_PREFIX + "Enter key reset listener attached to the textarea.");
            } else if (!textarea) {
                allElementsFound = false;
            }

            if (allElementsFound) {
                obs.disconnect(); // Все элементы найдены, прекращаем наблюдение
                console.log(SCRIPT_PREFIX + "All required UI elements found and listeners attached.");
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });

        scriptInitialized = true;
        console.log(SCRIPT_PREFIX + "Script loaded and fully initialized.");
    }

    function clickSendButton() {
        setTimeout(() => {
            const sendButton = document.getElementById(BUTTON_ID);
            if (sendButton) {
                if (sendButton.offsetParent !== null && !sendButton.disabled) {
                    autoClickCount++;
                    updateCounterDisplay();
                    sendButton.click();
                    console.log(SCRIPT_PREFIX + `Send button clicked automatically. New count: ${autoClickCount}`);
                } else {
                    console.warn(SCRIPT_PREFIX + `Send button found but is not clickable (Visible: ${sendButton.offsetParent !== null}, Disabled: ${sendButton.disabled}).`);
                }
            } else {
                console.warn(SCRIPT_PREFIX + `Send button with ID "${BUTTON_ID}" NOT FOUND.`);
            }
        }, 100);
    }

    // --- ЗАПУСК СКРИПТА ---
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeScript);
    } else {
        initializeScript();
    }
    window.addEventListener('load', () => {
        if (!scriptInitialized) {
            initializeScript();
        }
    });

})();