// ==UserScript== // @name Perplexity helper // @namespace Tiartyos // @match https://www.perplexity.ai/* // @grant none // @version 7.11 // @author Tiartyos, monnef // @description Simple script that adds buttons to Perplexity website for repeating request using Copilot. // @require https://code.jquery.com/jquery-3.6.0.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/lodash-fp/0.10.4/lodash-fp.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.8/clipboard.min.js // @require https://cdn.jsdelivr.net/npm/color2k@2.0.2/dist/index.unpkg.umd.js // @require https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jsondiffpatch/0.4.1/jsondiffpatch.umd.min.js // @require https://cdn.jsdelivr.net/npm/hex-to-css-filter@6.0.0/dist/umd/hex-to-css-filter.min.js // @require https://cdn.jsdelivr.net/npm/perplex-plus@0.0.72/dist/lib/perplex-plus.js // @homepageURL https://www.perplexity.ai/ // @license GPL-3.0-or-later // @downloadURL https://update.greasyfork.icu/scripts/469985/Perplexity%20helper.user.js // @updateURL https://update.greasyfork.icu/scripts/469985/Perplexity%20helper.meta.js // ==/UserScript== const PP = window.PP.noConflict(); const jq = PP.jq; const hexToCssFilter = window.HexToCSSFilter.hexToCSSFilter; const $c = (cls, parent) => jq(`.${cls}`, parent); const $i = (id, parent) => jq(`#${id}`, parent); const classNames = (...args) => args.filter(Boolean).join(' '); const takeStr = n => str => str.slice(0, n); const dropStr = n => str => str.slice(n); const dropRightStr = n => str => str.slice(0, -n); const filter = pred => xs => xs.filter(pred); const pipe = x => (...fns) => fns.reduce((acc, fn) => fn(acc), x); const nl = '\n'; const markdownConverter = new showdown.Converter({ tables: true }); let debugMode = false; const enableDebugMode = () => { debugMode = true; }; const userscriptName = 'Perplexity helper'; const logPrefix = `[${userscriptName}]`; const debugLog = (...args) => { if (debugMode) { console.debug(logPrefix, ...args); } }; let debugTags = false; const debugLogTags = (...args) => { if (debugTags) { console.debug(logPrefix, '[tags]', ...args); } }; let debugModalCreation = false; const logModalCreation = (...args) => { if (debugModalCreation) { console.debug(logPrefix, '[modalCreation]', ...args); } } let debugReplaceIconsInMenu = false; const logReplaceIconsInMenu = (...args) => { if (debugReplaceIconsInMenu) { console.debug(logPrefix, '[replaceIconsInMenu]', ...args); } } const log = (...args) => { console.log(logPrefix, ...args); }; const logError = (...args) => { console.error(logPrefix, ...args); }; const enableTagsDebugging = () => { debugTags = true; }; // Throttled debug logging to prevent spam // Stores unique message-parameter combinations for 60 seconds const debugLogThrottleCache = new Map(); const THROTTLE_WINDOW_MS = 60000; // 60 seconds const debugLogThrottled = (message, params = {}) => { if (!debugMode) return; const now = Date.now(); const messageKey = message; // Get or create message cache if (!debugLogThrottleCache.has(messageKey)) { debugLogThrottleCache.set(messageKey, new Map()); } const messageCache = debugLogThrottleCache.get(messageKey); // Create a key for the parameters using deep comparison const paramsKey = JSON.stringify(params, Object.keys(params).sort()); // Check if we've seen this exact combination recently if (messageCache.has(paramsKey)) { const lastLogged = messageCache.get(paramsKey); if (now - lastLogged < THROTTLE_WINDOW_MS) { return; // Skip logging, too recent } } // Log the message and update cache console.debug(logPrefix, '[throttled]', message, params); messageCache.set(paramsKey, now); // Clean up old entries (optional optimization) if (messageCache.size > 100) { // Prevent memory bloat const cutoff = now - THROTTLE_WINDOW_MS; for (const [key, timestamp] of messageCache.entries()) { if (timestamp < cutoff) { messageCache.delete(key); } } } }; ($ => { $.fn.nthParent = function (n) { let $p = $(this); if (!(n > -0)) { return $(); } let p = 1 + n; while (p--) { $p = $p.parent(); } return $p; }; })(jq); const initializePerplexityHelperHandlers = () => { const config = loadConfigOrDefault(); if (!config.toggleModeHooks) { log('toggleModeHooks is disabled, skipping initialization and uninstalling global hook'); PP.uninstallGlobalHook(); return; } // Register the condition checker PP.registerShouldBlockEnterHandler($wrapper => hasActiveToggledTagsForCurrentContext($wrapper)); // Register the handler for blocked enter key PP.registerBlockedEnterHandler(async ($textarea, $wrapper) => { // Flash the textarea indicator $textarea.removeClass(pulseFocusCls); $textarea.get(0).offsetHeight; // Force reflow $textarea.addClass(pulseFocusCls); setTimeout(() => $textarea.removeClass(pulseFocusCls), 400); // Apply toggled tags await applyToggledTagsOnSubmit($wrapper); await PP.sleep(500); // Find and click submit button const $submitBtn = PP.getSubmitButtonAnyExceptMic($wrapper); if ($submitBtn.length) { debugLog('blockedEnterInPromptAreaHandler: Clicking submit button after toggle tags', { $submitBtn, ariaLabel: $submitBtn.attr('aria-label'), dataTestId: $submitBtn.attr('data-testid') }); $submitBtn.click(); } else { debugLog('blockedEnterInPromptAreaHandler: No submit button found'); } }); }; // unpkg had quite often problems, tens of seconds to load, sometime 503 fails // const getLucideIconUrl = iconName => `https://unpkg.com/lucide-static@latest/icons/${iconName}.svg`; const getLucideIconUrl = (iconName) => `https://cdn.jsdelivr.net/npm/lucide-static@latest/icons/${iconName}.svg`; const getBrandIconInfo = (modelName = '', { preferBaseModelCompany = false } = {}) => { const normalizedModelName = modelName.toLowerCase(); // Try to get data from perplex-plus ModelDescriptor first try { const modelDescriptor = PP?.findModelDescriptorByName?.(modelName); if (modelDescriptor) { // Determine which company and color to use based on preferBaseModelCompany setting const useBaseModel = preferBaseModelCompany && modelDescriptor.baseModelCompany; const company = useBaseModel ? modelDescriptor.baseModelCompany : modelDescriptor.company; const companyColor = useBaseModel ? modelDescriptor.baseModelCompanyColor : modelDescriptor.companyColor; // Map company names to icon names const companyToIcon = { 'perplexity': 'perplexity', 'openai': 'openai', 'anthropic': 'claude', 'google': 'gemini', 'xai': 'xai', 'deepseek': 'deepseek', 'meta': 'meta' }; const iconName = companyToIcon[company]; // debugLog('getBrandIconInfo: Found icon for model', { modelName, preferBaseModelCompany, iconName, companyColor, modelDescriptor }); if (iconName && companyColor) { return { iconName, brandColor: companyColor }; } } } catch (error) { // Fallback to original logic if perplex-plus data is not available } debugLogThrottled(`getBrandIconInfo: No icon found for model. modelName = ${modelName}, preferBaseModelCompany = ${preferBaseModelCompany}`); // Original fallback logic if (normalizedModelName.includes('claude')) { return { iconName: 'claude', brandColor: '#D97757' }; } else if (normalizedModelName.includes('gpt') || normalizedModelName.startsWith('o')) { return { iconName: 'openai', brandColor: '#FFFFFF' }; } else if (normalizedModelName.includes('gemini')) { return { iconName: 'gemini', brandColor: '#1C69FF' }; } else if (normalizedModelName.includes('sonar') || normalizedModelName.includes('best') || normalizedModelName.includes('auto')) { return preferBaseModelCompany ? { iconName: 'meta', brandColor: '#1D65C1' } : { iconName: 'perplexity', brandColor: '#22B8CD' }; } else if (normalizedModelName.includes('r1')) { return preferBaseModelCompany ? { iconName: 'deepseek', brandColor: '#4D6BFE' } : { iconName: 'perplexity', brandColor: '#22B8CD' }; } else if (normalizedModelName.includes('grok')) { return { iconName: 'xai', brandColor: '#FFFFFF' }; } else if (normalizedModelName.includes('llama') || normalizedModelName.includes('meta')) { return { iconName: 'meta', brandColor: '#1D65C1' }; } else if (normalizedModelName.includes('anthropic')) { return { iconName: 'anthropic', brandColor: '#F1F0E8' }; } return null; }; const getTDesignIconUrl = iconName => `https://api.iconify.design/tdesign:${iconName}.svg`; const getLobeIconsUrl = iconName => `https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/${iconName}.svg`; const parseIconName = iconName => { if (!iconName.includes(':')) return { typePrefix: 'l', processedIconName: iconName }; const [typePrefix, processedIconName] = iconName.split(':'); return { typePrefix, processedIconName }; }; const getIconUrl = iconName => { const { typePrefix, processedIconName } = parseIconName(iconName); if (typePrefix === 'td') { return getTDesignIconUrl(processedIconName); } if (typePrefix === 'l') { return getLucideIconUrl(processedIconName); } throw new Error(`Unknown icon type: ${typePrefix}`); }; const pplxHelperTag = 'pplx-helper'; const genCssName = x => `${pplxHelperTag}--${x}`; const button = (id, icoName, title, extraClass) => ``; const upperButton = (id, icoName, title) => `