Perplexity Плейграунд Адвансед русский!

Добавляет расширенные функции в Perplexity Playground.

// ==UserScript==
// @name         Perplexity Плейграунд Адвансед русский!
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Добавляет расширенные функции в Perplexity Playground.
// @match        https://playground.perplexity.ai/
// @author       YouTubeDrawaria
// @grant        none
// @license      MIT
// @icon         https://playground.perplexity.ai/favicon.ico
// ==/UserScript==

(function() {
    'use strict';

    // Вы можете изменить этот селектор, если изменится id селектора модели
    const SELECT_MODEL_ID = 'lamma-select';
    const DEFAULT_MODEL_VALUE = 'sonar-reasoning-pro'; // Модель по умолчанию для выбора

    // Определяем CSS-селекторы для ключевых элементов страницы
    const SELECTORS = {
        CHAT_CONTAINER_ROOT: 'div.flex.h-full.min-h-screen.flex-col > div.flex.h-full.grow',
        CHAT_MESSAGES_SCROLL_CONTAINER: 'div.pt-md.md\\:pt-lg.grow.md\\:border-x',
        MESSAGE_BUBBLE_COMMON: '.px-md.py-sm.max-w-full.break-words.rounded-lg.border.text-left.shadow-sm',
        MESSAGE_TEXT_CONTAINER: '.prose',
        INPUT_TEXTAREA: 'textarea[placeholder="Ask anything…"]',
        TEXTAREA_PARENT_CONTAINER: 'div.bg-raised.w-full', // Родитель textarea для позиционирования счетчика
        HEADER_RIGHT_SECTION: '.gap-sm.flex.items-center', // Правая секция, где находятся "sonar" и "Try Perplexity"
        // Контейнер, который содержит кнопку корзины и grow-div textarea
        INPUT_CONTROLS_ROW: 'div.px-md.py-md.border-t.md\\:border-x > div.gap-x-sm.flex.items-center',
        CLEAR_CHAT_BUTTON: 'button[aria-label="Clear Chat"]', // Кнопка корзины
    };

    // Расширенные категоризированные промпты для выпадающего меню
    const ALL_CATEGORIZED_PROMPTS = {
        "Игровые промпты": [
            { name: "Простая HTML игра", text: `Создайте игру в одном HTML-файле. Не используйте data:image/png;base64. Генерируйте графику, используя фигуры и SVG.` },
            { name: "Полная игра", text: `Сгенерируйте ресурсы, спрайты, ассеты, звуковые эффекты, музыку, механики, концепции, игровые дизайны, идеи и особенности для полноценной игры. Будьте точны, умны и лаконичны.` },
            { name: "Воссоздать игру", text: `Создайте подробный промпт для ИИ, чтобы воссоздать существующую игру. Шаг за шагом объясните, как он должен подходить к воссозданию, включая анализ оригинальной игры, определение ключевых механик, создание ассетов, реализацию кода и фазы тестирования. Будьте тщательны в каждой детали.` },
            { name: "Сложная HTML игра", text: `Создайте игру в одном HTML-файле с большой картой, добавьте элементы, объекты, детали и лучшую графику. Будьте точны, умны и лаконичны. Используйте только фигуры и SVG для всей графики (без base64encoded или PNG изображений). Вся графика должна быть создана с использованием фигур и SVG-путей, без внешних ресурсов, с плавными анимациями и переходами, подходящей пошаговой боевой механикой, отзывчивыми элементами пользовательского интерфейса, системой управления здоровьем, четырьмя различными движениями с расчетом случайного урона, вражеским ИИ с базовой логикой атаки и визуальной обратной связью для атак и урона.` },
            { name: "Детализированная игра", text: `Улучшите, расширьте и усовершенствуйте существующую игру. Игра должна иметь большую карту и включать элементы, объекты, детали и лучшую графику, а также улучшенных и детализированных персонажей. Я хочу, чтобы вся игра была в одном файле. Не используйте base64encoded или PNG изображения; вы должны создавать графику с максимальной сложностью, детализацией и улучшением, используя только фигуры и SVG. Сделайте игру максимально хорошей и большой. Кроме того, добавьте больше типов платформ, создайте больше типов врагов, реализуйте различные эффекты усилений, установите систему уровней, разработайте различные окружения, разработайте более сложный ИИ врагов, сделайте движения игроков более плавными и улучшите пользовательский интерфейс как для игрока, так и для врагов.` }
        ],
        "Веб-промпты": [
            { name: "Современный веб-сайт", text: `Создайте код для современной целевой страницы веб-сайта, которая. Убедитесь, что она выглядит красиво и хорошо спроектирована.` }
        ],
        "Промпты персонажа": [
            { name: "Описание персонажа", text: `Сделайте длинное описание, описывающее все о персонаже с дополнительной подробной информацией. Сделайте профессиональное описание, подробно описывающее все об изображении с более подробной информацией.` }
        ],
        "Промпты песни": [
            { name: "Атрибуты песни", text: `Дайте мне атрибуты песни, разделенные запятыми. Атрибуты песни, разделенные запятыми.` }
        ],
        "Промпты Gemini": [
            { name: "Сгенерировать 4 изображения X", text: `Сгенерировать 4 новых разных [X] в 4 изображениях каждый.` }
        ],
        "Промпты для скриптинга/разработки": [
            { name: "Создать скрипт Drawaria", text: `Создайте полный скрипт Tampermonkey для drawaria.online со следующей начальной структурой:\n // ==UserScript==\n// @name New Userscript\n// @namespace http://tampermonkey.net/\n// @version 1.0\n// @description try to take over the world!\n// @author YouTubeDrawaria\n// @match https://drawaria.online/*\n// @grant none\n// @license MIT\n// @icon https://www.google.com/s2/favicons?sz=64&domain=drawaria.online\n// ==/UserScript==\n\n(function() {\n    'use strict';\n\n    // Ваш код здесь...\n})();\n` },
            { name: "Улучшить скрипт Drawaria", text: `Улучшите, обновите, максимизируйте, удивите, создайте реализм и высокий уровень детализации в скрипте для drawaria.online. Мне нужны элементы X на экране, музыка, эффекты, частицы, блики и хорошо анимированный и детализированный интерфейс со всем. Не используйте заполнители, .mp3 или data:image/png;base64. Вы должны создавать графику сами, без заменяемых файлов.` },
            { name: "Атрибуты игры (Drawaria)", text: `Дайте мне атрибуты игры. Включите: иконку игры (<link rel="icon" href="https://drawaria.online/avatar/cache/ab53c430-1b2c-11f0-af95-072f6d4ed084.1749767757401.jpg" type="image/x-icon">) и фоновую музыку с автовоспроизведением по клику: (<audio id="bg-music" src="https://www.myinstants.com/media/sounds/super-mini-juegos-2.mp3" loop></audio><script>const music = document.getElementById('bg-music'); document.body.addEventListener('click', () => { if (music.paused) { music.play(); } });</script>).` },
            { name: "Информация об API Cubic Engine", text: `Предоставьте информацию о широко используемых API, которые не размещены на Vercel, не имеют проблем с CORS при использовании из браузеров/оболочек, могут быть быстро интегрированы в Cubic Engine / Drawaria и являются бесплатными и готовыми к использованию.` },
            { name: "Интегрировать функцию Cubic Engine", text: `Для интеграции нового дополнения в модуль Cubic Engine мне нужен полный обновленный код функции. Это включает кнопку со всеми ее свойствами, триггеры с их идентификаторами, слушатели этого события и файлы, которые его выполняют. Предоставьте только код обновленной функции, а не код Cubic Engine с нуля.` }
        ]
    };

    let featuresInitialized = false; // Флаг для предотвращения двойной инициализации

    // SVG-иконка загрузки (похожа на корзину)
    const DOWNLOAD_ICON_SVG = `
        <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-download">
            <path d="M4 17v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"></path>
            <polyline points="7 11 12 16 17 11"></polyline>
            <line x1="12" y1="4" x2="12" y2="16"></line>
        </svg>
    `;

    /**
     * Устанавливает модель ИИ по умолчанию в селекторе 'lamma-select'.
     */
    function setDefaultModel() {
        const select = document.getElementById(SELECT_MODEL_ID);
        if (select && select.value !== DEFAULT_MODEL_VALUE) {
            select.value = DEFAULT_MODEL_VALUE;
            select.dispatchEvent(new Event('change', { bubbles: true }));
            console.log(`Perplexity Playground Advanced: Модель установлена на '${DEFAULT_MODEL_VALUE}'.`);
        }
    }

    /**
     * Создает HTML-кнопку с предопределенными стилями (серая тема).
     * @param {string} text - Текст кнопки.
     * @param {function} onClick - Функция, выполняемая при клике.
     * @param {string} [buttonColor='#4a4a50'] - Цвет фона кнопки.
     * @param {object} [styles={}] - Дополнительные CSS-стили.
     * @returns {HTMLButtonElement}
     */
    function createButton(text, onClick, buttonColor = '#4a4a50', styles = {}) {
        const button = document.createElement('button');
        button.textContent = text;
        button.style.cssText = `
            background-color: ${buttonColor};
            color: white;
            padding: 8px 12px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 14px;
            margin-left: 10px;
            transition: opacity 0.3s ease;
            white-space: nowrap;
            ${Object.entries(styles).map(([key, value]) => `${key}:${value};`).join('')}
        `;
        button.onmouseover = () => button.style.opacity = '0.8';
        button.onmouseout = () => button.style.opacity = '1';
        button.onclick = onClick;
        return button;
    }

    /**
     * Создает кнопку с иконкой с предопределенными стилями (круглая, серая).
     * @param {string} svgContent - SVG-код иконки.
     * @param {string} title - Текст подсказки при наведении (tooltip).
     * @param {function} onClick - Функция, выполняемая при клике.
     * @param {string} [buttonColor='#4a4a50'] - Цвет фона кнопки.
     * @param {object} [styles={}] - Дополнительные CSS-стили.
     * @returns {HTMLButtonElement}
     */
    function createIconButton(svgContent, title, onClick, buttonColor = '#4a4a50', styles = {}) {
        const button = document.createElement('button');
        button.innerHTML = svgContent;
        button.title = title;
        button.style.cssText = `
            background-color: ${buttonColor};
            color: white;
            padding: 0;
            border: none;
            border-radius: 9999px; /* Круглый */
            cursor: pointer;
            transition: opacity 0.3s ease, background-color 0.15s ease-in-out;
            display: flex;
            align-items: center;
            justify-content: center;
            height: 40px; /* Размер похож на кнопку корзины */
            width: 40px;  /* Квадратный */
            flex-shrink: 0;
            ${Object.entries(styles).map(([key, value]) => `${key}:${value};`).join('')}
        `;
        button.onmouseover = () => { button.style.opacity = '0.8'; button.style.backgroundColor = '#5c5c63'; }; // Слегка светлее при наведении
        button.onmouseout = () => { button.style.opacity = '1'; button.style.backgroundColor = buttonColor; }; // Вернуть к исходному цвету
        button.onclick = onClick;
        return button;
    }

    /**
     * Создает выпадающее меню (select) с группами опций (optgroups) для категоризации (серая тема).
     * @param {object} categorizedOptions - Объект с категориями и опциями.
     * @param {function} onSelect - Функция, выполняемая при выборе опции.
     * @param {string} [placeholder="Выбрать промпт"] - Текст заполнителя.
     * @param {string} [dropdownColor='#4a4a50'] - Цвет фона выпадающего списка.
     * @returns {HTMLSelectElement}
     */
    function createCategorizedDropdown(categorizedOptions, onSelect, placeholder = "Выбрать промпт", dropdownColor = '#4a4a50') {
        const select = document.createElement('select');
        select.style.cssText = `
            background-color: ${dropdownColor};
            color: white;
            padding: 8px 12px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 14px;
            margin-left: 10px;
            transition: opacity 0.3s ease;
            appearance: none;
            -webkit-appearance: none;
            -moz-appearance: none;
            background-image: url('data:image/svg+xml;utf8,<svg fill="white" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>');
            background-repeat: no-repeat;
            background-position: right 8px top 50%;
            background-size: 16px;
            min-width: 150px;
        `;

        const defaultOption = document.createElement('option');
        defaultOption.value = "";
        defaultOption.textContent = placeholder;
        defaultOption.disabled = true;
        defaultOption.selected = true;
        select.appendChild(defaultOption);

        for (const category in categorizedOptions) {
            const optgroup = document.createElement('optgroup');
            optgroup.label = category;
            categorizedOptions[category].forEach(opt => {
                const option = document.createElement('option');
                option.value = opt.text;
                option.textContent = opt.name;
                optgroup.appendChild(option);
            });
            select.appendChild(optgroup);
        }

        select.onchange = (event) => {
            if (event.target.value) {
                onSelect(event.target.value);
                event.target.value = ""; // Сбросить до заполнителя
            }
        };
        return select;
    }

    /**
     * Создает и отображает модальное окно.
     * @param {string} title - Заголовок модального окна.
     * @param {string} contentHtml - HTML-содержимое модального окна.
     */
    function showModal(title, contentHtml) {
        const existingModalOverlay = document.getElementById('perplexity-custom-modal-overlay');
        if (existingModalOverlay) {
            existingModalOverlay.remove();
        }

        const modalOverlay = document.createElement('div');
        modalOverlay.id = 'perplexity-custom-modal-overlay';
        modalOverlay.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.7);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 9999;
        `;

        const modalContent = document.createElement('div');
        modalContent.id = 'perplexity-custom-modal';
        modalContent.style.cssText = `
            background-color: #2b2b30;
            color: white;
            padding: 25px;
            border-radius: 12px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
            max-width: 80%;
            max-height: 80%;
            overflow-y: auto;
            display: flex;
            flex-direction: column;
            position: relative;
        `;

        const modalTitle = document.createElement('h3');
        modalTitle.textContent = title;
        modalTitle.style.cssText = `
            margin-top: 0;
            margin-bottom: 20px;
            color: #6366f1;
            font-size: 1.2em;
            text-align: center;
        `;

        const closeButton = document.createElement('button');
        closeButton.textContent = 'X';
        closeButton.style.cssText = `
            position: absolute;
            top: 10px;
            right: 15px;
            background: none;
            border: none;
            color: #aaa;
            font-size: 1.2em;
            cursor: pointer;
        `;
        closeButton.onclick = () => modalOverlay.remove();

        modalContent.appendChild(closeButton);
        modalContent.appendChild(modalTitle);

        const contentDiv = document.createElement('div');
        contentDiv.innerHTML = contentHtml;
        modalContent.appendChild(contentDiv);

        modalOverlay.appendChild(modalContent);
        document.body.appendChild(modalOverlay);
    }

    // --- Основные функции чата ---

    /** Получает текущее содержимое чата. */
    function getCurrentChatContent() {
        const chatBubbles = document.querySelectorAll(SELECTORS.MESSAGE_BUBBLE_COMMON);
        let chatContent = [];

        chatBubbles.forEach(bubble => {
            const isUserMessage = bubble.parentElement && bubble.parentElement.classList.contains('justify-end');
            let messageText = '';
            const proseElement = bubble.querySelector(SELECTORS.MESSAGE_TEXT_CONTAINER);
            messageText = proseElement ? proseElement.innerText.trim() : bubble.innerText.trim();

            if (messageText) {
                chatContent.push({
                    type: isUserMessage ? 'Пользователь' : 'Perplexity',
                    text: messageText,
                    timestamp: new Date().toISOString()
                });
            }
        });
        return chatContent;
    }

    /** Сохраняет текущую беседу в localStorage. */
    function saveCurrentChat() {
        const chatContent = getCurrentChatContent();
        if (chatContent.length === 0) {
            alert('Нет беседы для сохранения.');
            return;
        }

        const chatName = prompt("Введите имя для этой беседы:", `Чат ${new Date().toLocaleString()}`);
        if (chatName) {
            try {
                const savedChats = JSON.parse(localStorage.getItem('perplexity_playground_chats') || '[]');
                savedChats.push({ name: chatName, timestamp: new Date().toISOString(), messages: chatContent });
                localStorage.setItem('perplexity_playground_chats', JSON.stringify(savedChats));
                alert(`Беседа "${chatName}" успешно сохранена.`);
            } catch (e) {
                console.error("Ошибка при сохранении беседы:", e);
                alert("Ошибка при сохранении беседы.");
            }
        }
    }

    /** Загружает и отображает сохраненные беседы, позволяя просматривать или удалять их. */
    function loadSavedChats() {
        const savedChats = JSON.parse(localStorage.getItem('perplexity_playground_chats') || '[]');
        if (savedChats.length === 0) {
            alert('Нет сохраненных бесед.');
            return;
        }

        let chatListHtml = '<ul style="list-style-type: none; padding: 0;">';
        savedChats.forEach((chat, index) => {
            chatListHtml += `
                <li style="margin-bottom: 10px; background-color: #3a3a40; padding: 10px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center;">
                    <span style="font-size: 0.9em; color: #ccc;">${chat.name} (${new Date(chat.timestamp).toLocaleString()})</span>
                    <div>
                        <button class="view-chat-btn" data-index="${index}" style="background-color: #6366f1; color: white; border: none; padding: 6px 10px; border-radius: 6px; cursor: pointer; margin-right: 5px;">Просмотр</button>
                        <button class="delete-chat-btn" data-index="${index}" style="background-color: #dc3545; color: white; border: none; padding: 6px 10px; border-radius: 6px; cursor: pointer;">Удалить</button>
                    </div>
                </li>
            `;
        });
        chatListHtml += '</ul>';

        showModal('Сохраненные беседы', chatListHtml);

        document.querySelectorAll('.view-chat-btn').forEach(button => {
            button.onclick = (e) => {
                const index = e.target.dataset.index;
                const chatToView = savedChats[index];
                let chatViewHtml = '<div style="background-color: #1a1a1a; padding: 15px; border-radius: 8px; max-height: 400px; overflow-y: auto;">';
                chatToView.messages.forEach(msg => {
                    const align = msg.type === 'Пользователь' ? 'right' : 'left';
                    const bgColor = msg.type === 'Пользователь' ? '#007bff' : '#333';
                    chatViewHtml += `<div style="text-align: ${align}; margin-bottom: 10px;">
                                        <div style="display: inline-block; background-color: ${bgColor}; padding: 8px 12px; border-radius: 10px; max-width: 90%; word-wrap: break-word;">
                                            <strong style="color: ${msg.type === 'Пользователь' ? '#cceeff' : '#aaffaa'};">${msg.type}:</strong> ${msg.text}
                                        </div>
                                    </div>`;
                });
                chatViewHtml += '</div>';
                showModal(`Просмотр беседы: ${chatToView.name}`, chatViewHtml);
            };
        });

        document.querySelectorAll('.delete-chat-btn').forEach(button => {
            button.onclick = (e) => {
                const indexToDelete = parseInt(e.target.dataset.index);
                if (confirm(`Вы уверены, что хотите удалить беседу "${savedChats[indexToDelete].name}"?`)) {
                    savedChats.splice(indexToDelete, 1);
                    localStorage.setItem('perplexity_playground_chats', JSON.stringify(savedChats));
                    alert('Беседа удалена.');
                    document.getElementById('perplexity-custom-modal-overlay')?.remove();
                    loadSavedChats();
                }
            };
        });
    }

    /** Экспортирует текущую беседу в текстовый файл. */
    function exportChatToText() {
        const chatContent = getCurrentChatContent();
        if (chatContent.length === 0) {
            alert('Нет беседы для экспорта.');
            return;
        }

        let exportText = `--- Беседа Perplexity Playground (${new Date().toLocaleString()}) ---\n\n`;
        chatContent.forEach(msg => {
            exportText += `${msg.type}: ${msg.text}\n\n`;
        });
        exportText += `--- Конец беседы ---\n`;

        const blob = new Blob([exportText], { type: 'text/plain' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `perplexity_chat_${new Date().toISOString().replace(/[:.]/g, '-')}.txt`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
        alert('Беседа экспортирована в текстовый файл.');
    }

    /**
     * Устанавливает значение элемента ввода таким образом, чтобы React его распознал.
     * @param {HTMLElement} element - DOM-элемент input/textarea.
     * @param {string} value - Новое значение для установки.
     */
    function setNativeValue(element, value) {
        const valueSetter = Object.getOwnPropertyDescriptor(element.__proto__, 'value').set;
        const event = new Event('input', { bubbles: true });
        valueSetter.call(element, value);
        element.dispatchEvent(event);
    }

    /**
     * Вставляет предопределенный промпт в текстовое поле ввода.
     * @param {string} promptText - Текст промпта для вставки.
     */
    function handlePromptSelection(promptText) {
        const inputTextArea = document.querySelector(SELECTORS.INPUT_TEXTAREA);
        if (inputTextArea) {
            setNativeValue(inputTextArea, promptText);
            inputTextArea.focus();
        }
    }

    /** Настраивает счетчик символов и слов в текстовом поле. */
    function setupCharacterCounter() {
        const inputTextArea = document.querySelector(SELECTORS.INPUT_TEXTAREA);
        if (!inputTextArea) return;

        const container = inputTextArea.closest(SELECTORS.TEXTAREA_PARENT_CONTAINER);
        if (!container) return;

        if (document.getElementById('perplexity-char-word-counter')) {
            return; // Счетчик уже существует
        }

        const counterSpan = document.createElement('span');
        counterSpan.id = 'perplexity-char-word-counter';
        counterSpan.style.cssText = `
            position: absolute;
            bottom: 8px;
            right: 12px;
            font-size: 10px;
            color: #888;
            pointer-events: none;
            z-index: 10;
        `;
        if (getComputedStyle(container).position === 'static') {
            container.style.position = 'relative';
        }
        container.appendChild(counterSpan);

        const updateCounter = () => {
            const text = inputTextArea.value;
            const charCount = text.length;
            const wordCount = text.trim().split(/\s+/).filter(word => word.length > 0).length;
            counterSpan.textContent = `Символов: ${charCount} | Слов: ${wordCount}`;
        };

        inputTextArea.addEventListener('input', updateCounter);
        updateCounter(); // Инициализация счетчика
    }

    /**
     * Добавляет функцию редактирования и переотправки к пузырьку сообщения.
     * @param {HTMLElement} messageBubble - Пузырек сообщения, к которому нужно добавить функцию.
     */
    function addEditAndResendToMessage(messageBubble) {
        if (messageBubble.dataset.hasEditListener) {
            return; // Уже обработано
        }
        messageBubble.dataset.hasEditListener = 'true';

        // Разрешить редактирование/переотправку только сообщений ПОЛЬЗОВАТЕЛЯ
        const isUserMessage = messageBubble.parentElement && messageBubble.parentElement.classList.contains('justify-end');

        if (isUserMessage) {
            messageBubble.style.cursor = 'pointer';
            messageBubble.style.transition = 'filter 0.15s ease-in-out'; // Переход для фильтра

            messageBubble.addEventListener('click', function(event) {
                // event.stopPropagation(); // Раскомментировать, если не нужно, чтобы клик распространялся

                let messageText = '';
                const proseElement = messageBubble.querySelector(SELECTORS.MESSAGE_TEXT_CONTAINER);
                messageText = proseElement ? proseElement.innerText.trim() : messageBubble.innerText.trim();

                const inputTextArea = document.querySelector(SELECTORS.INPUT_TEXTAREA);
                if (inputTextArea && messageText) {
                    setNativeValue(inputTextArea, messageText);
                    inputTextArea.focus();
                }
            });

            // Визуальный эффект при наведении курсора (hover)
            messageBubble.onmouseenter = () => {
                 messageBubble.style.filter = 'brightness(1.1)'; // Слегка ярче
            };
            messageBubble.onmouseleave = () => {
                 messageBubble.style.filter = ''; // Вернуть к исходному состоянию
            };
        }
    }


    /** Настраивает наблюдателя для добавления функции редактирования/переотправки к новым сообщениям. */
    function setupEditAndResendObserver() {
        const chatContainer = document.querySelector(SELECTORS.CHAT_MESSAGES_SCROLL_CONTAINER);

        if (!chatContainer) {
            console.warn("Perplexity Playground Advanced Features: Контейнер сообщений для наблюдателя (Редактировать и переотправить) не найден.");
            return;
        }

        const observer = new MutationObserver((mutationsList) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === 1) { // Узел элемента
                            if (node.matches(SELECTORS.MESSAGE_BUBBLE_COMMON)) {
                                addEditAndResendToMessage(node);
                            }
                            node.querySelectorAll(SELECTORS.MESSAGE_BUBBLE_COMMON).forEach(addEditAndResendToMessage);
                        }
                    });
                }
            }
        });

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

        // Применить функцию к существующим сообщениям при загрузке
        document.querySelectorAll(SELECTORS.MESSAGE_BUBBLE_COMMON).forEach(addEditAndResendToMessage);
        console.log("Perplexity Playground Advanced Features: Функция 'Редактировать и переотправить' настроена.");
    }

    // --- Функция: Импорт текстовых файлов и OCR ---

    // Динамическая загрузка Tesseract.js:
    function loadTesseractJs() {
        return new Promise((resolve, reject) => {
            if (window.Tesseract) {
                console.log("Tesseract.js уже загружен.");
                resolve();
                return;
            }
            console.log("Загрузка Tesseract.js с CDN...");
            const script = document.createElement('script');
            script.src = "https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js";
            script.onload = () => {
                if (window.Tesseract) {
                    console.log("Tesseract.js успешно загружен.");
                    resolve();
                } else {
                    console.error("Tesseract.js не найден после загрузки скрипта.");
                    reject(new Error("Tesseract.js не найден после загрузки скрипта."));
                }
            };
            script.onerror = (e) => {
                console.error("Ошибка при загрузке Tesseract.js:", e);
                reject(new Error("Ошибка при загрузке Tesseract.js с CDN."));
            };
            document.head.appendChild(script);
        });
    }

    // Динамическая загрузка pdf.js:
    function loadPdfJs() {
        return new Promise((resolve, reject) => {
            if (window.pdfjsLib) {
                console.log("pdf.js уже загружен.");
                resolve();
                return;
            }
            console.log("Загрузка pdf.js с CDN...");
            const script = document.createElement('script');
            script.src = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.2.67/pdf.min.js";
            script.onload = function() {
                try {
                    if (window.pdfjsLib) {
                        // Убедитесь, что workerSrc настроен правильно
                        window.pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.2.67/pdf.worker.min.js";
                        console.log("pdf.js и workerSrc успешно настроены.");
                        resolve();
                    } else {
                        console.error("pdfjsLib не найден после загрузки скрипта.");
                        reject(new Error("pdfjsLib не найден после загрузки скрипта."));
                    }
                } catch (e) {
                    console.error("Ошибка при настройке pdfjsLib.GlobalWorkerOptions.workerSrc:", e);
                    reject(e);
                }
            };
            script.onerror = (e) => {
                console.error("Ошибка при загрузке pdf.js:", e);
                reject(new Error("Ошибка при загрузке pdf.js с CDN."));
            };
            document.head.appendChild(script);
        });
    }

    // Динамическая загрузка mammoth.js:
    function loadMammothJs() {
        return new Promise((resolve, reject) => {
            if (window.mammoth) {
                console.log("mammoth.js уже загружен.");
                resolve();
                return;
            }
            console.log("Загрузка mammoth.js с CDN...");
            const script = document.createElement('script');
            script.src = "https://unpkg.com/mammoth/mammoth.browser.min.js";
            script.onload = () => {
                if (window.mammoth) {
                    console.log("mammoth.js успешно загружен.");
                    resolve();
                } else {
                    console.error("mammoth не найден после загрузки скрипта.");
                    reject(new Error("mammoth не найден после загрузки скрипта."));
                }
            };
            script.onerror = (e) => {
                console.error("Ошибка при загрузке mammoth.js:", e);
                reject(new Error("Ошибка при загрузке mammoth.js с CDN."));
            };
            document.head.appendChild(script);
        });
    }


    /**
     * Читает файл как текст.
     * @param {File} file - Файл для чтения.
     * @returns {Promise<string>} Промис, разрешающийся содержимым файла.
     */
    function readFileAsText(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = (e) => resolve(e.target.result);
            reader.onerror = (e) => {
                console.error(`Ошибка FileReader для файла ${file.name}:`, e);
                reject(e);
            };
            reader.readAsText(file);
        });
    }

    /**
     * Извлекает текст из PDF-файла.
     * @param {File} file - PDF-файл для обработки.
     * @returns {Promise<string>} Промис, разрешающийся извлеченным текстом.
     */
    async function extractTextFromPdf(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = async (e) => {
                try {
                    const typedarray = new Uint8Array(e.target.result);
                    const loadingTask = window.pdfjsLib.getDocument({ data: typedarray });
                    const pdf = await loadingTask.promise;
                    let text = '';
                    for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
                        const page = await pdf.getPage(pageNum);
                        const content = await page.getTextContent();
                        // Объединить текстовые элементы пробелом и добавить разрыв строки на страницу
                        text += content.items.map(item => item.str).join(' ') + '\n';
                    }
                    resolve(text);
                } catch (err) {
                    console.error(`Ошибка при извлечении текста из PDF ${file.name}:`, err);
                    reject(new Error(`Ошибка при обработке PDF: ${err.message || err}. Убедитесь, что это не отсканированный PDF без текстового слоя.`));
                }
            };
            reader.onerror = (e) => {
                console.error(`Ошибка FileReader при чтении PDF ${file.name}:`, e);
                reject(e);
            };
            reader.readAsArrayBuffer(file);
        });
    }

    /**
     * Извлекает текст из DOCX-файла (Word).
     * @param {File} file - DOCX-файл для обработки.
     * @returns {Promise<string>} Промис, разрешающийся извлеченным текстом.
     */
    async function extractTextFromDocx(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = async (e) => {
                try {
                    const arrayBuffer = e.target.result;
                    // mammoth.js возвращает объект с 'value' (текст) и 'messages'
                    const result = await mammoth.extractRawText({ arrayBuffer });
                    if (result.messages.length > 0) {
                        console.warn(`Сообщения mammoth.js при обработке ${file.name}:`, result.messages);
                    }
                    resolve(result.value.trim());
                } catch (err) {
                    console.error(`Ошибка при извлечении текста из DOCX ${file.name}:`, err);
                    reject(new Error(`Ошибка при обработке DOCX: ${err.message || err}.`));
                }
            };
            reader.onerror = (e) => {
                console.error(`Ошибка FileReader при чтении DOCX ${file.name}:`, e);
                reject(e);
            };
            reader.readAsArrayBuffer(file);
        });
    }

    /**
     * Обрабатывает список файлов и вставляет их в текстовое поле.
     * @param {FileList} files - Файлы для обработки.
     */
    async function processDroppedFiles(files) {
        const inputTextArea = document.querySelector(SELECTORS.INPUT_TEXTAREA);
        if (!inputTextArea) {
            console.warn("Текстовое поле ввода не найдено.");
            return;
        }

        let allContent = '';
        const importButton = document.getElementById('perplexity-import-button'); // Получить кнопку импорта

        // Сохранить оригинальный заголовок и цвет кнопки для восстановления позже
        const originalButtonTitle = importButton ? importButton.title : '';
        const originalButtonBackgroundColor = importButton ? importButton.style.backgroundColor : '';

        for (const file of files) {
            const textFileExtensions = new Set([
                'txt', 'html', 'htm', 'css', 'js', 'json', 'csv', 'xml', 'md', 'log',
                'yaml', 'yml', 'py', 'java', 'c', 'cpp', 'h', 'hpp', 'go', 'php',
                'rb', 'sh', 'bat', 'ps1', 'psm1', 'ini', 'cfg', 'conf', 'env', 'rs', 'ts', 'jsx', 'tsx', 'vue'
            ]);
            const textMimeTypes = [
                'text/', 'application/json', 'application/xml', 'application/javascript',
                'application/x-sh', 'application/x-python', 'application/x-yaml'
            ];
            const imageFileExtensions = new Set(['png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp']);

            const fileNameParts = file.name.split('.');
            const fileExtension = fileNameParts.length > 1 ? fileNameParts.pop().toLowerCase() : '';

            const isKnownTextFile = textFileExtensions.has(fileExtension) || textMimeTypes.some(type => file.type.startsWith(type));
            const isImage = imageFileExtensions.has(fileExtension) || file.type.startsWith('image/');
            const isPdf = fileExtension === 'pdf' || file.type === 'application/pdf';
            const isDocx = fileExtension === 'docx' || file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';

            if (isKnownTextFile) {
                try {
                    const content = await readFileAsText(file);
                    if (allContent !== '') {
                        allContent += `\n\n--- Содержимое файла: ${file.name} ---\n\n`; // Четкий разделитель
                    }
                    allContent += content;
                } catch (error) {
                    console.error(`Ошибка при чтении текстового файла ${file.name}:`, error);
                    alert(`Не удалось прочитать файл: ${file.name}. Возможно, это бинарный, поврежденный или несовместимой кодировки файл. Пропущен. Подробности в консоли.`);
                }
            } else if (isImage) {
                if (importButton) {
                    importButton.style.backgroundColor = '#6366f1'; // Цвет "активный"
                    importButton.title = `Загрузка OCR для изображения: ${file.name}...`;
                }
                try {
                    await loadTesseractJs(); // Дождаться загрузки Tesseract.js
                    const { data: { text } } = await Tesseract.recognize(
                        file,
                        'spa+eng', // Поддерживает испанский и английский. Настройте по необходимости.
                        {
                            logger: m => {
                                if (importButton && m.status === 'recognizing') {
                                    importButton.title = `OCR ${file.name}: ${Math.round(m.progress * 100)}%`;
                                }
                            }
                        }
                    );
                    if (allContent !== '') {
                        allContent += `\n\n--- Текст, извлеченный из изображения: ${file.name} ---\n\n`;
                    }
                    allContent += text.trim();
                } catch (error) {
                    console.error(`Ошибка при обработке изображения ${file.name} с помощью OCR:`, error);
                    alert(`Не удалось извлечь текст из изображения: ${file.name}. Ошибка: ${error.message}. Подробности в консоли.`);
                }
            } else if (isPdf) {
                if (importButton) {
                    importButton.style.backgroundColor = '#6366f1';
                    importButton.title = `Загрузка PDF.js для: ${file.name}...`;
                }
                try {
                    await loadPdfJs(); // Дождаться загрузки pdf.js
                    importButton.title = `Обработка PDF: ${file.name}...`; // Обновить статус
                    const text = await extractTextFromPdf(file);
                    if (allContent !== '') {
                        allContent += `\n\n--- Текст, извлеченный из PDF: ${file.name} ---\n\n`;
                    }
                    allContent += text.trim();
                } catch (error) {
                    console.error(`Ошибка при извлечении текста из PDF ${file.name}:`, error);
                    alert(`Не удалось извлечь текст из PDF: ${file.name}. Ошибка: ${error.message}. Подробности в консоли.`);
                }
            } else if (isDocx) {
                if (importButton) {
                    importButton.style.backgroundColor = '#6366f1';
                    importButton.title = `Загрузка Mammoth.js для: ${file.name}...`;
                }
                try {
                    await loadMammothJs(); // Дождаться загрузки mammoth.js
                    importButton.title = `Обработка Word: ${file.name}...`; // Обновить статус
                    const text = await extractTextFromDocx(file);
                    if (allContent !== '') {
                        allContent += `\n\n--- Текст, извлеченный из Word: ${file.name} ---\n\n`;
                    }
                    allContent += text;
                } catch (error) {
                    console.error(`Ошибка при извлечении текста из Word ${file.name}:`, error);
                    alert(`Не удалось извлечь текст из Word: ${file.name}. Ошибка: ${error.message}. Подробности в консоли.`);
                }
            }
            else {
                alert(`Этот скрипт предназначен для извлечения текста из простых файлов (таких как код, текстовые документы и т.д.), изображений, PDF или документов Word (.docx). Он не может извлекать содержимое из сложных бинарных файлов другого типа. Пропущен: ${file.name}`);
            }
        }

        // Восстановить заголовок и цвет кнопки после обработки всех файлов
        if (importButton) {
            importButton.title = originalButtonTitle;
            importButton.style.backgroundColor = originalButtonBackgroundColor;
        }

        if (allContent) {
            // Добавляет содержимое в textarea, сохраняя существующее содержимое
            setNativeValue(inputTextArea, inputTextArea.value + (inputTextArea.value ? '\n\n' : '') + allContent);
            inputTextArea.focus();
            inputTextArea.scrollTop = inputTextArea.scrollHeight; // Прокрутить до конца
        }
    }

    /** Настраивает кнопку импорта файлов с функцией Drag & Drop. */
    function setupImportButton() {
        const inputControlsRow = document.querySelector(SELECTORS.INPUT_CONTROLS_ROW);
        const clearChatButton = document.querySelector(SELECTORS.CLEAR_CHAT_BUTTON);

        if (!inputControlsRow || !clearChatButton) {
            console.warn("Perplexity Playground Advanced Features: Контейнер элементов управления или кнопка корзины для добавления кнопки импорта не найдены.");
            return;
        }

        if (document.getElementById('perplexity-import-button')) {
            return; // Кнопка уже существует
        }

        // Создать скрытый input для файла
        const fileInput = document.createElement('input');
        fileInput.type = 'file';
        fileInput.multiple = true;
        // Разрешает выбор текстовых файлов, изображений для OCR, PDF и DOCX
        fileInput.accept = `
            .txt,.html,.htm,.css,.js,.json,.csv,.xml,.md,.log,.yaml,.yml,.py,.java,.c,.cpp,.h,.hpp,.go,.php,.rb,.sh,.bat,.ps1,.psm1,.ini,.cfg,.conf,.env,.rs,.ts,.jsx,.tsx,.vue,
            .png,.jpg,.jpeg,.bmp,.gif,.webp,
            .pdf,application/pdf,
            .docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document,
            text/*,application/json,application/xml,application/javascript,application/x-sh,application/x-python,application/x-yaml,image/*
        `.replace(/\s/g, ''); // Удаляет пробелы для чистого строкового значения

        // Создать кнопку-иконку для импорта
        const importButton = createIconButton(
            DOWNLOAD_ICON_SVG,
            'Импортировать текстовые файлы, изображения, PDF или Word (перетащите или нажмите)',
            () => fileInput.click()
        );
        importButton.id = 'perplexity-import-button'; // Добавить ID кнопке

        // Слушатели событий для Drag & Drop на кнопке
        importButton.addEventListener('dragover', (e) => {
            e.preventDefault();
            e.stopPropagation();
            importButton.style.border = '2px dashed #6366f1'; // Пунктирная синяя рамка
            importButton.style.backgroundColor = '#5c5c63';
        });

        importButton.addEventListener('dragleave', (e) => {
            e.stopPropagation();
            importButton.style.border = 'none'; // Отменить
            importButton.style.backgroundColor = '#4a4a50';
        });

        importButton.addEventListener('drop', (e) => {
            e.preventDefault();
            e.stopPropagation();
            importButton.style.border = 'none';
            importButton.style.backgroundColor = '#4a4a50';

            if (e.dataTransfer.files.length > 0) {
                processDroppedFiles(e.dataTransfer.files);
            }
        });

        // Слушатель события для выбора файлов через диалог
        fileInput.addEventListener('change', (event) => {
            if (event.target.files.length > 0) {
                processDroppedFiles(event.target.files);
                event.target.value = ''; // Очистить input для возможности последовательных выборов
            }
        });

        // Создать новый контейнер для кнопок слева (корзина и импорт)
        const leftButtonsContainer = document.createElement('div');
        leftButtonsContainer.style.display = 'flex';
        leftButtonsContainer.style.flexDirection = 'column';
        leftButtonsContainer.style.gap = '8px'; // Промежуток между кнопками (gap-sm)
        leftButtonsContainer.style.alignItems = 'flex-start'; // Выровнять кнопки по левому краю в столбце

        // Переместить кнопку корзины в этот новый контейнер
        clearChatButton.remove(); // Удалить из оригинального положения
        leftButtonsContainer.appendChild(clearChatButton);

        // Добавить новую кнопку импорта в этот контейнер
        leftButtonsContainer.appendChild(importButton);

        // Вставить новый контейнер на место оригинальной кнопки корзины
        inputControlsRow.prepend(leftButtonsContainer);

        console.log("Perplexity Playground Advanced Features: Кнопка 'Импорт' и корзина сгруппированы.");
    }


    // --- Инициализация скрипта ---
    function initializeFeatures() {
        if (featuresInitialized) {
            return; // Предотвращает двойную инициализацию
        }
        featuresInitialized = true;

        // Установить модель по умолчанию (должно быть сделано после того, как select появится в DOM)
        setDefaultModel();

        const headerRightSection = document.querySelector(SELECTORS.HEADER_RIGHT_SECTION);
        if (headerRightSection) {
            // Ищем существующие кнопки, чтобы вставить наши перед ними
            const existingSonarButton = headerRightSection.querySelector('a[href="https://sonar.perplexity.ai"]');

            // Добавить пользовательские кнопки в обратном порядке, чтобы они отображались слева направо
            // Желаемый порядок: выпадающий список промптов, Загрузить, Сохранить, Экспорт, sonar, Try Perplexity

            // Добавить кнопку Экспорт беседы (серая)
            const exportButton = createButton('Экспорт чата', exportChatToText);
            headerRightSection.insertBefore(exportButton, existingSonarButton);

            // Добавить кнопку Сохранить беседу (серая)
            const saveButton = createButton('Сохранить чат', saveCurrentChat);
            headerRightSection.insertBefore(saveButton, existingSonarButton);

            // Добавить кнопку Загрузить беседу (серая)
            const loadButton = createButton('Загрузить чат', loadSavedChats);
            headerRightSection.insertBefore(loadButton, existingSonarButton);

            // Добавить комбинированное выпадающее меню промптов (серое)
            const promptsDropdown = createCategorizedDropdown(ALL_CATEGORIZED_PROMPTS, handlePromptSelection, "Расширенные промпты");
            headerRightSection.insertBefore(promptsDropdown, existingSonarButton);

            console.log("Perplexity Playground Advanced Features: Кнопки заголовка и меню промптов добавлены.");
        } else {
            console.warn("Perplexity Playground Advanced Features: Правая секция заголовка для добавления кнопок не найдена.");
        }

        // Настроить счетчик символов/слов
        setupCharacterCounter();

        // Настроить функцию редактирования и переотправки для пузырьков сообщений
        setupEditAndResendObserver();

        // Настроить кнопку импорта текстовых файлов
        setupImportButton();
    }

    // Использовать MutationObserver, чтобы убедиться, что DOM полностью загружен и ключевые элементы доступны.
    const appRootObserver = new MutationObserver((mutations, obs) => {
        // Проверяем, присутствуют ли критические элементы UI
        if (
            document.querySelector(SELECTORS.INPUT_TEXTAREA) &&
            document.querySelector(SELECTORS.CHAT_MESSAGES_SCROLL_CONTAINER) &&
            document.getElementById(SELECT_MODEL_ID) &&
            document.querySelector(SELECTORS.INPUT_CONTROLS_ROW) &&
            document.querySelector(SELECTORS.CLEAR_CHAT_BUTTON) // Убедиться, что кнопка корзины присутствует
        ) {
            initializeFeatures();
            obs.disconnect(); // Отключить наблюдатель после успешной инициализации
        }
    });

    // Начать наблюдение за 'body' на предмет изменений в DOM
    appRootObserver.observe(document.body, { childList: true, subtree: true });

    // Запасной вариант: Если элементы уже присутствуют при выполнении скрипта (например, быстрая перезагрузка)
    if (
        document.querySelector(SELECTORS.INPUT_TEXTAREA) &&
        document.querySelector(SELECTORS.CHAT_MESSAGES_SCROLL_CONTAINER) &&
        document.getElementById(SELECT_MODEL_ID) &&
        document.querySelector(SELECTORS.INPUT_CONTROLS_ROW) &&
        document.querySelector(SELECTORS.CLEAR_CHAT_BUTTON)
    ) {
        initializeFeatures();
    }

})();