您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Añade funcionalidades avanzadas a Perplexity Playground!
当前为
// ==UserScript== // @name Perplexity Playground Advanced // @namespace http://tampermonkey.net/ // @version 1.3 // @description Añade funcionalidades avanzadas a Perplexity Playground! // @match https://playground.perplexity.ai/ // @author YouTubeDrawaria // @grant none // @license MIT // @icon https://playground.perplexity.ai/favicon.ico // ==/UserScript== (function() { 'use strict'; // Puedes cambiar este selector si cambia el id del selector de modelo const SELECT_MODEL_ID = 'lamma-select'; const DEFAULT_MODEL_VALUE = 'sonar-pro'; // Modelo por defecto a seleccionar // Define los selectores CSS para los elementos clave de la página 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', // Padre del textarea para posicionar el contador HEADER_RIGHT_SECTION: '.gap-sm.flex.items-center', // Sección derecha donde están "sonar" y "Try Perplexity" // Contenedor que tiene el botón de la papelera y el grow-div del 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"]', // Botón de la papelera }; // Prompts avanzados categorizados para el menú desplegable const ALL_CATEGORIZED_PROMPTS = { "Prompts de Juego": [ { name: "Juego Simple HTML", text: `Crea un juego en un solo archivo HTML. No uses data:image/png;base64. Genera los gráficos usando formas y SVG.` }, { name: "Juego Completo", text: `Genera recursos, sprites, assets, sfx, música, mecánicas, conceptos, diseños de juego, ideas y características para un juego completo. Sé preciso, inteligente y conciso.` }, { name: "Recrear Juego", text: `Crea un prompt detallado para que una IA recree un juego existente. Explica paso a paso cómo debe abordar la recreación, incluyendo el análisis del juego original, la identificación de mecánicas clave, la creación de assets, la implementación del código y las fases de prueba. Sé minucioso en cada detalle.` }, { name: "Juego Complejo HTML", text: `Crea un juego en un solo archivo HTML con un mapa grande, añade elementos, objetos, detalles y los mejores gráficos. Sé preciso, inteligente y conciso. Usa solo formas y SVG para todos los gráficos (sin base64encoded o imágenes PNG). Todos los gráficos deben ser creados usando formas y trazados SVG, sin recursos externos, con animaciones y transiciones fluidas, mecánicas de batalla por turnos adecuadas, elementos de UI responsivos, un sistema de gestión de salud, cuatro movimientos diferentes con cálculo de daño aleatorio, IA enemiga con lógica de ataque básica, y retroalimentación visual para ataques y daño.` }, { name: "Juego Detallado", text: `Mejora, expande y perfecciona un juego existente. El juego debe tener un mapa grande, y debe incluir elementos, objetos, detalles y los mejores gráficos, junto con personajes mejorados y detallados. Quiero que todo el juego esté contenido en un solo archivo. No uses imágenes base64encoded o PNG; debes crear los gráficos con la máxima complejidad, detalle y mejora posible, utilizando únicamente formas y SVG. Haz que el juego sea lo mejor y más grande que pueda ser. Además, añade más tipos de plataformas, crea más tipos de enemigos, implementa diferentes efectos de power-up, establece un sistema de niveles, diseña distintos entornos, desarrolla una IA enemiga más compleja, haz que los movimientos de los jugadores sean más suaves y mejora la interfaz de usuario tanto para el jugador como para los enemigos.` } ], "Prompts Web": [ { name: "Web Moderna", text: `Crea el código para una landing page de un sitio web moderno que . Make the code for landing page. Make sure it looks nice and well designed` } ], "Prompts Personaje": [ { name: "Descripción Personaje", text: `Haz una descripción larga describiendo todo sobre el personaje con información extra detallada. Haz una descripción profesional describiendo detalladamente todo sobre la imagen con información más detallada.` } ], "Prompts Canción": [ { name: "Atributos de Canción", text: `Dame los atributos de la canción separados por comas. Atributos de la canción separados por comas.` } ], "Prompts Gemini": [ { name: "Generar 4 Imágenes X", text: `Genera 4 nuevas [X] diferentes en 4 imágenes cada una.` } ], "Prompts de Scripting/Desarrollo": [ { name: "Crear Script Drawaria", text: `Crea un script tampermonkey completo para drawaria.online con la siguiente estructura inicial:\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 // Your code here...\n})();\n` }, { name: "Script Drawaria Avanzado", text: "Crea un script tampermonkey completo para drawaria.online con funcionalidades avanzadas: efectos visuales, partículas, animaciones, interfaz mejorada, y características especiales. No uses placeholders ni archivos externos." }, { name: "Mejorar Script Drawaria", text: `Mejora, actualiza, maximiza, sorprende, crea realismo y alto nivel de detalle en el script para drawaria.online. Quiero elementos de X en pantalla, música, efectos, partículas, brillos y una interfaz bien animada y detallada con todo. No uses placeholders, .mp3 ni data:image/png;base64. Debes crear los gráficos tú mismo, sin archivos reemplazables.` }, { name: "Atributos de Juego", text: `Dame los atributos de un juego. Incluye: icono del juego (<link rel="icon" href="https://drawaria.online/avatar/cache/ab53c430-1b2c-11f0-af95-072f6d4ed084.1749767757401.jpg" type="image/x-icon">) y música de fondo con reproducción automática al hacer clic: (<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 Info", text: `Proporciona información sobre APIs ampliamente utilizadas que no estén alojadas en Vercel, no presenten problemas con CORS al usarlas desde navegadores/shell, se puedan integrar rápidamente en Cubic Engine / Drawaria, y sean gratuitas y de uso inmediato.` }, { name: "Integrar Función Cubic Engine", text: `Para integrar una nueva adición a un módulo de Cubic Engine, necesito el código completo actualizado de la función. Esto incluye el botón con todas sus propiedades, los activadores con sus IDs, los listeners de este evento y los archivos que lo ejecutan. Solo proporciona el código de la función actualizada, no el código de Cubic Engine desde cero.` } ] }; let featuresInitialized = false; // Flag para evitar la doble inicialización // Icono de descarga SVG (similar al de la papelera) 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> `; /** * Establece el modelo de IA por defecto en el selector '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: Modelo establecido a '${DEFAULT_MODEL_VALUE}'.`); } } /** * Crea un botón HTML con estilos predefinidos (tema gris). * @param {string} text - El texto del botón. * @param {function} onClick - La función a ejecutar al hacer clic. * @param {string} [buttonColor='#4a4a50'] - El color de fondo del botón. * @param {object} [styles={}] - Estilos CSS adicionales. * @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; } /** * Crea un botón de icono con estilos predefinidos (redondo, gris). * @param {string} svgContent - El código SVG del icono. * @param {string} title - El texto de la sugerencia al pasar el ratón (tooltip). * @param {function} onClick - La función a ejecutar al hacer clic. * @param {string} [buttonColor='#4a4a50'] - El color de fondo del botón. * @param {object} [styles={}] - Estilos CSS adicionales. * @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; /* Redondo */ cursor: pointer; transition: opacity 0.3s ease, background-color 0.15s ease-in-out; display: flex; align-items: center; justify-content: center; height: 40px; /* Tamaño similar al botón de la papelera */ width: 40px; /* Cuadrado */ flex-shrink: 0; ${Object.entries(styles).map(([key, value]) => `${key}:${value};`).join('')} `; button.onmouseover = () => { button.style.opacity = '0.8'; button.style.backgroundColor = '#5c5c63'; }; // Ligeramente más claro en hover button.onmouseout = () => { button.style.opacity = '1'; button.style.backgroundColor = buttonColor; }; // Volver al color original button.onclick = onClick; return button; } /** * Crea un menú desplegable (select) con optgroups para categorizar opciones (tema gris). * @param {object} categorizedOptions - Objeto con categorías y opciones. * @param {function} onSelect - Función a ejecutar al seleccionar una opción. * @param {string} [placeholder="Seleccionar Prompt"] - Texto del placeholder. * @param {string} [dropdownColor='#4a4a50'] - Color de fondo del desplegable. * @returns {HTMLSelectElement} */ function createCategorizedDropdown(categorizedOptions, onSelect, placeholder = "Seleccionar Prompt", 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 = ""; // Restablecer a placeholder } }; return select; } /** * Crea y muestra un modal superpuesto. * @param {string} title - Título del modal. * @param {string} contentHtml - Contenido HTML del modal. */ 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); } // --- Funcionalidades principales del chat --- /** Obtiene el contenido actual del chat. */ 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 ? 'User' : 'Perplexity', text: messageText, timestamp: new Date().toISOString() }); } }); return chatContent; } /** Guarda la conversación actual en el localStorage. */ function saveCurrentChat() { const chatContent = getCurrentChatContent(); if (chatContent.length === 0) { alert('No hay conversación para guardar.'); return; } const chatName = prompt("Introduce un nombre para esta conversación:", `Chat ${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(`Conversación "${chatName}" guardada con éxito.`); } catch (e) { console.error("Error al guardar la conversación:", e); alert("Error al guardar la conversación."); } } } /** Carga y muestra las conversaciones guardadas, permitiendo verlas o eliminarlas. */ function loadSavedChats() { const savedChats = JSON.parse(localStorage.getItem('perplexity_playground_chats') || '[]'); if (savedChats.length === 0) { alert('No hay conversaciones guardadas.'); 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;">Ver</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;">Eliminar</button> </div> </li> `; }); chatListHtml += '</ul>'; showModal('Conversaciones Guardadas', 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 === 'User' ? 'right' : 'left'; const bgColor = msg.type === 'User' ? '#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 === 'User' ? '#cceeff' : '#aaffaa'};">${msg.type}:</strong> ${msg.text} </div> </div>`; }); chatViewHtml += '</div>'; showModal(`Ver Conversación: ${chatToView.name}`, chatViewHtml); }; }); document.querySelectorAll('.delete-chat-btn').forEach(button => { button.onclick = (e) => { const indexToDelete = parseInt(e.target.dataset.index); if (confirm(`¿Estás seguro de que quieres eliminar la conversación "${savedChats[indexToDelete].name}"?`)) { savedChats.splice(indexToDelete, 1); localStorage.setItem('perplexity_playground_chats', JSON.stringify(savedChats)); alert('Conversación eliminada.'); document.getElementById('perplexity-custom-modal-overlay')?.remove(); loadSavedChats(); } }; }); } /** Exporta la conversación actual a un archivo de texto. */ function exportChatToText() { const chatContent = getCurrentChatContent(); if (chatContent.length === 0) { alert('No hay conversación para exportar.'); return; } let exportText = `--- Conversación Perplexity Playground (${new Date().toLocaleString()}) ---\n\n`; chatContent.forEach(msg => { exportText += `${msg.type}: ${msg.text}\n\n`; }); exportText += `--- Fin de la Conversación ---\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('Conversación exportada a un archivo de texto.'); } /** * Establece el valor de un elemento de entrada de forma que React lo reconozca. * @param {HTMLElement} element - El elemento DOM del input/textarea. * @param {string} value - El nuevo valor a establecer. */ 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); } /** * Inserta un prompt predefinido en el área de texto de entrada. * @param {string} promptText - El texto del prompt a insertar. */ function handlePromptSelection(promptText) { const inputTextArea = document.querySelector(SELECTORS.INPUT_TEXTAREA); if (inputTextArea) { setNativeValue(inputTextArea, promptText); inputTextArea.focus(); } } /** Configura el contador de caracteres y palabras en el área de texto. */ 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; // Ya existe el contador } 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 = `Chars: ${charCount} | Words: ${wordCount}`; }; inputTextArea.addEventListener('input', updateCounter); updateCounter(); // Inicializa el contador } /** * Añade la funcionalidad de editar y reenviar a una burbuja de mensaje. * @param {HTMLElement} messageBubble - La burbuja de mensaje a la que añadir la función. */ function addEditAndResendToMessage(messageBubble) { if (messageBubble.dataset.hasEditListener) { return; // Ya procesado } messageBubble.dataset.hasEditListener = 'true'; // Solo permitir la edición/reenvío de los mensajes del USUARIO 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'; // Transición para el filtro messageBubble.addEventListener('click', function(event) { // event.stopPropagation(); // Descomenta si no quieres que el clic se propague 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(); } }); // Efecto visual al pasar el ratón (hover) messageBubble.onmouseenter = () => { messageBubble.style.filter = 'brightness(1.1)'; // Ligeramente más brillante }; messageBubble.onmouseleave = () => { messageBubble.style.filter = ''; // Volver al estado original }; } } /** Configura el observador para añadir la funcionalidad de editar/reenviar a nuevos mensajes. */ function setupEditAndResendObserver() { const chatContainer = document.querySelector(SELECTORS.CHAT_MESSAGES_SCROLL_CONTAINER); if (!chatContainer) { console.warn("Perplexity Playground Advanced Features: No se encontró el contenedor de mensajes para el observador (Editar y Reenviar)."); 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) { // Nodo de elemento if (node.matches(SELECTORS.MESSAGE_BUBBLE_COMMON)) { addEditAndResendToMessage(node); } node.querySelectorAll(SELECTORS.MESSAGE_BUBBLE_COMMON).forEach(addEditAndResendToMessage); } }); } } }); observer.observe(chatContainer, { childList: true, subtree: true }); // Aplica la función a los mensajes existentes al cargar document.querySelectorAll(SELECTORS.MESSAGE_BUBBLE_COMMON).forEach(addEditAndResendToMessage); console.log("Perplexity Playground Advanced Features: Funcionalidad 'Editar y Reenviar' configurada."); } // --- Funcionalidad: Importar Archivos de Texto y OCR --- // Añadir Tesseract.js dinámicamente: function loadTesseractJs() { return new Promise((resolve, reject) => { if (window.Tesseract) { console.log("Tesseract.js ya está cargado."); resolve(); return; } console.log("Cargando Tesseract.js desde 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 cargado con éxito."); resolve(); } else { console.error("Tesseract.js no se encontró después de la carga del script."); reject(new Error("Tesseract.js no se encontró después de la carga del script.")); } }; script.onerror = (e) => { console.error("Error al cargar Tesseract.js:", e); reject(new Error("Error al cargar Tesseract.js desde CDN.")); }; document.head.appendChild(script); }); } // Añadir pdf.js dinámicamente: function loadPdfJs() { return new Promise((resolve, reject) => { if (window.pdfjsLib) { console.log("pdf.js ya está cargado."); resolve(); return; } console.log("Cargando pdf.js desde 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) { // Asegúrate de que el workerSrc se configure correctamente window.pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.2.67/pdf.worker.min.js"; console.log("pdf.js y workerSrc configurados con éxito."); resolve(); } else { console.error("pdfjsLib no se encontró después de la carga del script."); reject(new Error("pdfjsLib no se encontró después de la carga del script.")); } } catch (e) { console.error("Error al configurar pdfjsLib.GlobalWorkerOptions.workerSrc:", e); reject(e); } }; script.onerror = (e) => { console.error("Error al cargar pdf.js:", e); reject(new Error("Error al cargar pdf.js desde CDN.")); }; document.head.appendChild(script); }); } // Añadir mammoth.js dinámicamente: function loadMammothJs() { return new Promise((resolve, reject) => { if (window.mammoth) { console.log("mammoth.js ya está cargado."); resolve(); return; } console.log("Cargando mammoth.js desde 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 cargado con éxito."); resolve(); } else { console.error("mammoth no se encontró después de la carga del script."); reject(new Error("mammoth no se encontró después de la carga del script.")); } }; script.onerror = (e) => { console.error("Error al cargar mammoth.js:", e); reject(new Error("Error al cargar mammoth.js desde CDN.")); }; document.head.appendChild(script); }); } /** * Lee un archivo como texto. * @param {File} file - El archivo a leer. * @returns {Promise<string>} Promesa que resuelve con el contenido del archivo. */ 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 error for file ${file.name}:`, e); reject(e); }; reader.readAsText(file); }); } /** * Extrae texto de un archivo PDF. * @param {File} file - El archivo PDF a procesar. * @returns {Promise<string>} Promesa que resuelve con el texto extraído. */ 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(); // Unir los ítems de texto con un espacio y añadir un salto de línea por página text += content.items.map(item => item.str).join(' ') + '\n'; } resolve(text); } catch (err) { console.error(`Error durante la extracción de texto de PDF ${file.name}:`, err); reject(new Error(`Error al procesar el PDF: ${err.message || err}. Asegúrate de que no sea un PDF escaneado sin capa de texto.`)); } }; reader.onerror = (e) => { console.error(`FileReader error al leer PDF ${file.name}:`, e); reject(e); }; reader.readAsArrayBuffer(file); }); } /** * Extrae texto de un archivo DOCX (Word). * @param {File} file - El archivo DOCX a procesar. * @returns {Promise<string>} Promesa que resuelve con el texto extraído. */ 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 devuelve un objeto con 'value' (el texto) y 'messages' const result = await mammoth.extractRawText({ arrayBuffer }); if (result.messages.length > 0) { console.warn(`Mensajes de mammoth.js al procesar ${file.name}:`, result.messages); } resolve(result.value.trim()); } catch (err) { console.error(`Error durante la extracción de texto de DOCX ${file.name}:`, err); reject(new Error(`Error al procesar el DOCX: ${err.message || err}.`)); } }; reader.onerror = (e) => { console.error(`FileReader error al leer DOCX ${file.name}:`, e); reject(e); }; reader.readAsArrayBuffer(file); }); } /** * Procesa una lista de archivos y los inserta en el área de texto. * @param {FileList} files - Los archivos a procesar. */ async function processDroppedFiles(files) { const inputTextArea = document.querySelector(SELECTORS.INPUT_TEXTAREA); if (!inputTextArea) { console.warn("No se encontró el área de texto de entrada."); return; } let allContent = ''; const importButton = document.getElementById('perplexity-import-button'); // Obtener el botón de importación // Guardar el título original y color para restaurarlos después 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--- Contenido del archivo: ${file.name} ---\n\n`; // Separador claro } allContent += content; } catch (error) { console.error(`Error al leer el archivo de texto ${file.name}:`, error); alert(`No se pudo leer el archivo: ${file.name}. Podría ser un archivo binario, corrupto o de codificación no compatible. Se ignoró. Consulta la consola para más detalles.`); } } else if (isImage) { if (importButton) { importButton.style.backgroundColor = '#6366f1'; // Color de "activo" importButton.title = `Cargando OCR para imagen: ${file.name}...`; } try { await loadTesseractJs(); // Esperar a que Tesseract.js esté cargado const { data: { text } } = await Tesseract.recognize( file, 'spa+eng', // Soporta español e inglés. Ajusta según necesites. { logger: m => { if (importButton && m.status === 'recognizing') { importButton.title = `OCR ${file.name}: ${Math.round(m.progress * 100)}%`; } } } ); if (allContent !== '') { allContent += `\n\n--- Texto extraído de imagen: ${file.name} ---\n\n`; } allContent += text.trim(); } catch (error) { console.error(`Error al procesar la imagen ${file.name} con OCR:`, error); alert(`No se pudo extraer texto de la imagen: ${file.name}. Error: ${error.message}. Consulta la consola para más detalles.`); } } else if (isPdf) { if (importButton) { importButton.style.backgroundColor = '#6366f1'; importButton.title = `Cargando PDF.js para: ${file.name}...`; } try { await loadPdfJs(); // Esperar a que pdf.js esté cargado importButton.title = `Procesando PDF: ${file.name}...`; // Actualizar estado const text = await extractTextFromPdf(file); if (allContent !== '') { allContent += `\n\n--- Texto extraído de PDF: ${file.name} ---\n\n`; } allContent += text.trim(); } catch (error) { console.error(`Error al extraer texto de PDF ${file.name}:`, error); alert(`No se pudo extraer texto del PDF: ${file.name}. Error: ${error.message}. Consulta la consola para más detalles.`); } } else if (isDocx) { if (importButton) { importButton.style.backgroundColor = '#6366f1'; importButton.title = `Cargando Mammoth.js para: ${file.name}...`; } try { await loadMammothJs(); // Esperar a que mammoth.js esté cargado importButton.title = `Procesando Word: ${file.name}...`; // Actualizar estado const text = await extractTextFromDocx(file); if (allContent !== '') { allContent += `\n\n--- Texto extraído de Word: ${file.name} ---\n\n`; } allContent += text; } catch (error) { console.error(`Error al extraer texto de Word ${file.name}:`, error); alert(`No se pudo extraer texto del Word: ${file.name}. Error: ${error.message}. Consulta la consola para más detalles.`); } } else { alert(`Este script está diseñado para extraer texto de archivos planos (como código, documentos de texto, etc.), imágenes, PDFs o documentos de Word (.docx). No puede extraer contenido de archivos binarios complejos de otro tipo. Se ignoró: ${file.name}`); } } // Restaurar el título y color del botón después de procesar todos los archivos if (importButton) { importButton.title = originalButtonTitle; importButton.style.backgroundColor = originalButtonBackgroundColor; } if (allContent) { // Añade el contenido al textarea, manteniendo el contenido existente setNativeValue(inputTextArea, inputTextArea.value + (inputTextArea.value ? '\n\n' : '') + allContent); inputTextArea.focus(); inputTextArea.scrollTop = inputTextArea.scrollHeight; // Desplázate al final } } /** Configura el botón de importación de archivos con drag-and-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: No se encontró el contenedor de controles o el botón de la papelera para añadir el botón de importación."); return; } if (document.getElementById('perplexity-import-button')) { return; // Ya existe el botón } // Crear el input de archivo oculto const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.multiple = true; // Permite la selección de archivos de texto, imágenes para OCR, PDF y 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, ''); // Elimina espacios para un string limpio // Crear el botón de icono para importar const importButton = createIconButton( DOWNLOAD_ICON_SVG, 'Importar archivos de texto, imagen, PDF o Word (arrastrar y soltar o clic)', () => fileInput.click() ); importButton.id = 'perplexity-import-button'; // Añadir ID al botón // Event listeners para Drag & Drop en el botón importButton.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); importButton.style.border = '2px dashed #6366f1'; // Borde punteado azul importButton.style.backgroundColor = '#5c5c63'; }); importButton.addEventListener('dragleave', (e) => { e.stopPropagation(); importButton.style.border = 'none'; // Revertir 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); } }); // Event listener para cuando se seleccionan archivos a través del diálogo fileInput.addEventListener('change', (event) => { if (event.target.files.length > 0) { processDroppedFiles(event.target.files); event.target.value = ''; // Limpiar el input para permitir selecciones consecutivas } }); // Crear un nuevo contenedor para los botones de la izquierda (papelera e importar) const leftButtonsContainer = document.createElement('div'); leftButtonsContainer.style.display = 'flex'; leftButtonsContainer.style.flexDirection = 'column'; leftButtonsContainer.style.gap = '8px'; // Espacio entre los botones (gap-sm) leftButtonsContainer.style.alignItems = 'flex-start'; // Alinear los botones a la izquierda en la columna // Mover el botón de la papelera a este nuevo contenedor clearChatButton.remove(); // Quitar de su posición original leftButtonsContainer.appendChild(clearChatButton); // Añadir el nuevo botón de importar a este contenedor leftButtonsContainer.appendChild(importButton); // Insertar el nuevo contenedor en el lugar original del botón de la papelera inputControlsRow.prepend(leftButtonsContainer); console.log("Perplexity Playground Advanced Features: Botón 'Importar' y papelera agrupados."); } // --- Inicialización del Script --- function initializeFeatures() { if (featuresInitialized) { return; // Evita la doble inicialización } featuresInitialized = true; // Establecer el modelo por defecto (debe hacerse después de que el select esté en el DOM) setDefaultModel(); const headerRightSection = document.querySelector(SELECTORS.HEADER_RIGHT_SECTION); if (headerRightSection) { // Busca los botones existentes para insertar los nuestros antes de ellos const existingSonarButton = headerRightSection.querySelector('a[href="https://sonar.perplexity.ai"]'); // Añade los botones personalizados en orden inverso para que aparezcan de izquierda a derecha // Deseado: Prompts dropdown, Cargar, Guardar, Exportar, sonar, Try Perplexity // Añadir botón de Exportar Conversación (gris) const exportButton = createButton('Exportar Chat', exportChatToText); headerRightSection.insertBefore(exportButton, existingSonarButton); // Añadir botón de Guardar Conversación (gris) const saveButton = createButton('Guardar Chat', saveCurrentChat); headerRightSection.insertBefore(saveButton, existingSonarButton); // Añadir botón de Cargar Conversación (gris) const loadButton = createButton('Cargar Chat', loadSavedChats); headerRightSection.insertBefore(loadButton, existingSonarButton); // Añadir menú desplegable de Prompts Combinados (gris) const promptsDropdown = createCategorizedDropdown(ALL_CATEGORIZED_PROMPTS, handlePromptSelection, "Prompts Avanzados"); headerRightSection.insertBefore(promptsDropdown, existingSonarButton); console.log("Perplexity Playground Advanced Features: Botones de cabecera y menú de prompts añadidos."); } else { console.warn("Perplexity Playground Advanced Features: No se encontró la sección derecha del encabezado para añadir los botones."); } // Configurar contador de caracteres/palabras setupCharacterCounter(); // Configurar funcionalidad de Editar y Reenviar para burbujas de mensaje setupEditAndResendObserver(); // Configurar botón de Importar Archivos de Texto setupImportButton(); } // Usar MutationObserver para asegurar que el DOM esté completamente cargado y los elementos clave disponibles. const appRootObserver = new MutationObserver((mutations, obs) => { // Verifica si los elementos críticos de la UI están presentes 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) // Asegurarse de que el botón de la papelera esté presente ) { initializeFeatures(); obs.disconnect(); // Desconectar el observador después de la inicialización exitosa } }); // Iniciar la observación del 'body' para cambios en el DOM appRootObserver.observe(document.body, { childList: true, subtree: true }); // Fallback: Si los elementos ya están presentes cuando el script se ejecuta (ej. recarga rápida) 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(); } })();