您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Добавляет расширенные функции в 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(); } })();