Sync between Sexy.AI and SillyTavern optimized

Enhanced and optimized integration between SillyTavern and Sexy.AI

// ==UserScript==
// @name         Sync between Sexy.AI and SillyTavern optimized
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  Enhanced and optimized integration between SillyTavern and Sexy.AI
// @author       You
// @match        https://sexy.ai/workflow*
// @match        https://staticui.sexy.ai/*
// @match        http://ducninh.top:8000/*
// @match        http://127.0.0.1:8000/*
// @match        http://*/*:8000/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// ==/UserScript==

(function() {
    'use strict';

    console.log("Script started on URL:", window.location.href);

    const isSexyAI = window.location.href.includes('sexy.ai') || window.location.href.includes('staticui.sexy.ai');
    const isSillyTavern = window.location.href.includes(':8000');

    // Utility Functions
    function createStyledButton(text, onClick) {
        const button = document.createElement('button');
        button.textContent = text;
        button.style.cssText = `
            padding: 5px 8px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
            opacity: 0.8;
            transition: all 0.3s;
            margin-left: 5px;
        `;
        button.addEventListener('mouseover', () => button.style.opacity = '1');
        button.addEventListener('mouseout', () => button.style.opacity = '0.8');
        button.addEventListener('click', onClick);
        return button;
    }

    function showNotification(message) {
        const overlay = document.createElement('div');
        overlay.style.cssText = `
            position: fixed;
            top: 10px;
            right: 10px;
            background-color: #4CAF50;
            color: white;
            padding: 8px;
            border-radius: 4px;
            z-index: 10000;
            opacity: 0;
            transition: opacity 0.3s;
        `;
        overlay.textContent = message;
        document.body.appendChild(overlay);

        requestAnimationFrame(() => {
            overlay.style.opacity = '1';
            setTimeout(() => {
                overlay.style.opacity = '0';
                setTimeout(() => overlay.remove(), 300);
            }, 1500);
        });
    }

    // Extract original text from message
    function extractOriginalText(messageNode) {
        const originalText = messageNode.getAttribute('data-original-text');
        if (originalText) {
            return originalText;
        }

        const messageText = messageNode.querySelector('.mes_text');
        if (!messageText) return '';

        const clone = messageText.cloneNode(true);
        const buttonContainer = clone.querySelector('.button-container');
        if (buttonContainer) {
            buttonContainer.remove();
        }

        return clone.textContent.trim();
    }

    // SexyAI Implementation
    if (isSexyAI) {
        if (window.location.href.includes('staticui.sexy.ai')) {
            const promptButton = createStyledButton('Get Prompt', () => {
                const prompt = GM_getValue('st_prompt', null);
                if (prompt) {
                    const positiveInput = document.querySelector('textarea') ||
                                        document.querySelector('input[type="text"]');

                    if (positiveInput) {
                        positiveInput.value = prompt;
                        const event = new Event('input', { bubbles: true });
                        positiveInput.dispatchEvent(event);
                        GM_setValue('st_prompt', null);
                        promptButton.style.backgroundColor = '#2196F3';
                        promptButton.textContent = 'Prompt Added';
                        setTimeout(() => {
                            promptButton.style.backgroundColor = '#4CAF50';
                            promptButton.textContent = 'Get Prompt';
                        }, 2000);
                    }
                }
            });
            promptButton.style.cssText += 'position: fixed; right: 20px; top: 80px;';
            document.body.appendChild(promptButton);
        }

        // Optimized image handling for SexyAI
        document.addEventListener('click', (e) => {
            if (e.target.tagName === 'IMG') {
                const allImages = document.querySelectorAll('img');
                const latestImages = Array.from(allImages)
                    .slice(-4)
                    .map(img => img.src);

                GM_setValue('sexyai_images', JSON.stringify(latestImages));
                showNotification('Images Synced');
            }
        }, true);
    }

    // SillyTavern Implementation
    else if (isSillyTavern) {
function showImageModal(imageUrl) {
    const existingModal = document.querySelector('.image-modal-container');
    if (existingModal) {
        existingModal.remove();
    }

    // Lấy ảnh từ storage với định dạng mới đã tối ưu
    const images = JSON.parse(GM_getValue('sexyai_images', '[]'));
    if (images.length === 0) return;

    const container = document.createElement('div');
    container.className = 'image-modal-container';
    container.style.cssText = `
        position: fixed;
        top: 20px;
        right: 20px;
        z-index: 10000;
        background: rgba(0, 0, 0, 0.8);
        padding: 10px;
        border-radius: 10px;
        max-width: 300px;
    `;

    // Single image element for better performance
    const imgElement = new Image();
    imgElement.style.cssText = `
        width: 100%;
        height: auto;
        max-height: 400px;
        object-fit: contain;
        border-radius: 5px;
    `;

    let currentIndex = imageUrl ? images.indexOf(imageUrl) : 0;
    if (currentIndex === -1) currentIndex = 0;

    function updateImage() {
        imgElement.src = images[currentIndex];
        // Preload next image
        const nextIndex = (currentIndex + 1) % images.length;
        new Image().src = images[nextIndex];
    }

    function switchImage(newIndex) {
        currentIndex = (newIndex + images.length) % images.length;
        requestAnimationFrame(updateImage);
    }

    const navigationContainer = document.createElement('div');
    navigationContainer.style.cssText = `
        display: flex;
        justify-content: space-between;
        margin-top: 10px;
    `;

    const prevButton = document.createElement('button');
    const nextButton = document.createElement('button');
    [prevButton, nextButton].forEach(button => {
        button.style.cssText = `
            background: #4CAF50;
            border: none;
            color: white;
            padding: 5px 15px;
            border-radius: 3px;
            cursor: pointer;
            margin: 0 5px;
            font-size: 16px;
            transition: background-color 0.2s;
        `;
    });
    prevButton.textContent = '←';
    nextButton.textContent = '→';

    prevButton.onclick = () => switchImage(currentIndex - 1);
    nextButton.onclick = () => switchImage(currentIndex + 1);

    const closeButton = document.createElement('button');
    closeButton.textContent = '×';
    closeButton.style.cssText = `
        position: absolute;
        top: -10px;
        right: -10px;
        background: #4CAF50;
        border: none;
        color: white;
        width: 20px;
        height: 20px;
        border-radius: 50%;
        font-size: 14px;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 0;
        transition: background-color 0.2s;
    `;

    closeButton.onmouseover = () => closeButton.style.backgroundColor = '#45a049';
    closeButton.onmouseout = () => closeButton.style.backgroundColor = '#4CAF50';

    closeButton.onclick = () => {
        container.remove();
        const showImageButton = document.querySelector('#show_image_button');
        if (showImageButton) {
            showImageButton.style.color = '';
            showImageButton.style.opacity = '0.7';
        }
    };

    // Keyboard navigation
    const keyHandler = (e) => {
        if (!container.isConnected) return;
        switch(e.key) {
            case 'ArrowLeft': switchImage(currentIndex - 1); break;
            case 'ArrowRight': switchImage(currentIndex + 1); break;
            case 'Escape': closeButton.click(); break;
        }
    };

    document.addEventListener('keydown', keyHandler);
    container.addEventListener('remove', () => {
        document.removeEventListener('keydown', keyHandler);
    });

    // Assemble modal
    container.appendChild(closeButton);
    container.appendChild(imgElement);
    navigationContainer.appendChild(prevButton);
    navigationContainer.appendChild(nextButton);
    container.appendChild(navigationContainer);
    document.body.appendChild(container);

    // Initial load
    updateImage();
}
        function addControlButtons() {
            if (document.querySelector('#show_image_button') || document.querySelector('#send_prompt_button')) {
                return;
            }

            const extensionsButton = document.querySelector('#extensionsMenuButton');
            const optionsButton = document.querySelector('#options_button');

            if (extensionsButton && optionsButton) {
                const showImageButton = document.createElement('div');
                showImageButton.id = 'show_image_button';
                showImageButton.className = 'fa-solid fa-eye interactable';
                showImageButton.title = 'Show/Hide Images';
                showImageButton.style.cssText = `
                    display: flex;
                    cursor: pointer;
                    opacity: 0.7;
                    margin: 0 5px;
                    font-size: 18px;
                    transition: all 0.3s;
                `;
                showImageButton.tabIndex = "0";

                const sendPromptButton = document.createElement('div');
                sendPromptButton.id = 'send_prompt_button';
                sendPromptButton.className = 'fa-solid fa-paper-plane interactable';
                sendPromptButton.title = 'Send Prompt';
                sendPromptButton.style.cssText = `
                    display: flex;
                    cursor: pointer;
                    opacity: 0.7;
                    margin: 0 5px;
                    font-size: 18px;
                    transition: all 0.3s;
                `;
                sendPromptButton.tabIndex = "0";

                let isShowingImages = false;

                showImageButton.addEventListener('click', () => {
                    isShowingImages = !isShowingImages;
                    showImageButton.style.color = isShowingImages ? 'var(--accent-color, #4CAF50)' : '';
                    showImageButton.style.opacity = isShowingImages ? '1' : '0.7';

                    if (isShowingImages) {
                        const images = JSON.parse(GM_getValue('sexyai_images', '[]'));
                        if (images.length > 0) {
                            showImageModal(images[0]);
                        }
                    } else {
                        const existingModal = document.querySelector('.image-modal-container');
                        if (existingModal) {
                            existingModal.remove();
                        }
                    }
                });

                sendPromptButton.addEventListener('click', () => {
                    const messages = document.getElementsByClassName('mes');
                    const lastMessage = messages[messages.length - 1];
                    if (lastMessage) {
                        const text = extractOriginalText(lastMessage);
                        const match = text.match(/image###([^#]+)###/);
                        if (match) {
                            const prompt = match[1].trim();
                            GM_setValue('st_prompt', prompt);
                            sendPromptButton.style.color = 'var(--accent-color, #4CAF50)';
                            setTimeout(() => {
                                sendPromptButton.style.color = '';
                            }, 2000);
                        }
                    }
                });

                optionsButton.parentNode.insertBefore(showImageButton, optionsButton.nextSibling);
                extensionsButton.parentNode.insertBefore(sendPromptButton, extensionsButton.nextSibling);
            }
        }

        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                if (mutation.removedNodes.length > 0) {
                    if (!document.querySelector('#show_image_button') || !document.querySelector('#send_prompt_button')) {
                        addControlButtons();
                    }
                }
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        // Initial setup
        let checkInterval = setInterval(() => {
            if (document.querySelector('#options_button') && document.querySelector('#extensionsMenuButton')) {
                addControlButtons();
                if (document.querySelector('#show_image_button') && document.querySelector('#send_prompt_button')) {
                    clearInterval(checkInterval);
                }
            }
        }, 1000);
    }
})();