您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Simple script that adds buttons to Perplexity website for repeating request using Copilot.
当前为
// ==UserScript== // @name Perplexity helper // @namespace Tiartyos // @match https://www.perplexity.ai/* // @grant none // @version 2.9 // @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/[email protected]/dist/index.unpkg.umd.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/showdown.min.js // @homepageURL https://www.perplexity.ai/ // @license GPL-3.0-or-later // ==/UserScript== const jq = $.noConflict(); const $c = (cls, parent) => jq(`.${cls}`, parent); const $i = (id, parent) => jq(`#${id}`, parent); const takeStr = n => str => str.slice(0, n); const dropStr = n => str => str.slice(n); const nl = '\n'; const markdownConverter = new showdown.Converter(); let debugMode = false; const enableDebugMode = () => { debugMode = true; }; const logPrefix = '[Perplexity helper]'; const debugLog = (...args) => { if (debugMode) { console.debug(logPrefix, ...args); } } let debugTags = false; const debugLogTags = (...args) => { if (debugTags) { console.debug(logPrefix, '[tags]', ...args); } } const enableTagsDebugging = () => { debugTags = true; } ($ => { $.fn.nthParent = function (n) { let $p = $(this); if (!(n > -0)) { return $() } let p = 1 + n; while (p--) { $p = $p.parent(); } return $p; }; })(jq); const button = (id, icoName, title, extraClass) => `<button title="${title}" type="button" id="${id}" class="btn-helper bg-super dark:bg-superDark dark:text-backgroundDark text-white hover:opacity-80 font-sans focus:outline-none outline-none outline-transparent transition duration-300 ease-in-out font-sans select-none items-center relative group justify-center text-center items-center rounded-full cursor-point active:scale-95 origin-center whitespace-nowrap inline-flex text-base aspect-square h-10 ${extraClass}" > <div class="flex items-center leading-none justify-center gap-xs"> ${icoName} </div></button>`; const upperButton = (id, icoName, title) => ` <div title="${title}" id="${id}" class="border rounded-full px-sm py-xs flex items-center gap-x-sm border-borderMain/60 dark:border-borderMainDark/60 divide-borderMain dark:divide-borderMainDark ring-borderMain dark:ring-borderMainDark bg-transparent cursor-pointer"><div class="border-borderMain/60 dark:border-borderMainDark/60 divide-borderMain dark:divide-borderMainDark ring-borderMain dark:ring-borderMainDark bg-transparent"><div class="flex items-center gap-x-xs transition duration-300 select-none hover:text-superAlt light font-sans text-sm font-medium text-textOff dark:text-textOffDark selection:bg-super selection:text-white dark:selection:bg-opacity-50 selection:bg-opacity-70"><div class="">${icoName}<path fill="currentColor" d="M64 288L39.8 263.8C14.3 238.3 0 203.8 0 167.8C0 92.8 60.8 32 135.8 32c36 0 70.5 14.3 96 39.8L256 96l24.2-24.2c25.5-25.5 60-39.8 96-39.8C451.2 32 512 92.8 512 167.8c0 36-14.3 70.5-39.8 96L448 288 256 480 64 288z"></path></svg></div><div></div></div></div></div> ` const textButton = (id, text, title) => ` <button title="${title}" id="${id}" type="button" class="bg-super text-white hover:opacity-80 font-sans focus:outline-none outline-none transition duration-300 ease-in-out font-sans select-none items-center relative group justify-center rounded-md cursor-point active:scale-95 origin-center whitespace-nowrap inline-flex text-sm px-sm font-medium h-8"> <div class="flex items-center leading-none justify-center gap-xs"><span class="flex items-center relative ">${text}</span></div></button> ` const icoColor = '#1F1F1F'; const robotIco = `<svg style="width: 23px; fill: ${icoColor};" viewBox="0 0 640 512" xmlns="http://www.w3.org/2000/svg"><path d="m32 224h32v192h-32a31.96166 31.96166 0 0 1 -32-32v-128a31.96166 31.96166 0 0 1 32-32zm512-48v272a64.06328 64.06328 0 0 1 -64 64h-320a64.06328 64.06328 0 0 1 -64-64v-272a79.974 79.974 0 0 1 80-80h112v-64a32 32 0 0 1 64 0v64h112a79.974 79.974 0 0 1 80 80zm-280 80a40 40 0 1 0 -40 40 39.997 39.997 0 0 0 40-40zm-8 128h-64v32h64zm96 0h-64v32h64zm104-128a40 40 0 1 0 -40 40 39.997 39.997 0 0 0 40-40zm-8 128h-64v32h64zm192-128v128a31.96166 31.96166 0 0 1 -32 32h-32v-192h32a31.96166 31.96166 0 0 1 32 32z"/></svg>`; const robotRepeatIco = `<svg style="width: 23px; fill: ${icoColor};" viewBox="0 0 640 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/"> <path d="M442.179,325.051L442.179,459.979C442.151,488.506 418.685,511.972 390.158,512L130.053,512C101.525,511.972 78.06,488.506 78.032,459.979L78.032,238.868C78.032,203.208 107.376,173.863 143.037,173.863L234.095,173.863L234.095,121.842C234.095,107.573 245.836,95.832 260.105,95.832C274.374,95.832 286.116,107.573 286.116,121.842L286.116,173.863L309.247,173.863C321.515,245.71 373.724,304.005 442.179,325.051ZM26.011,277.905L52.021,277.905L52.021,433.968L25.979,433.968C11.727,433.968 -0,422.241 -0,407.989L-0,303.885C-0,289.633 11.727,277.905 25.979,277.905L26.011,277.905ZM468.19,331.092C478.118,332.676 488.289,333.497 498.65,333.497C505.935,333.497 513.126,333.091 520.211,332.299L520.211,407.989C520.211,422.241 508.483,433.968 494.231,433.968L468.19,433.968L468.19,331.092ZM208.084,407.958L156.063,407.958L156.063,433.968L208.084,433.968L208.084,407.958ZM286.116,407.958L234.095,407.958L234.095,433.968L286.116,433.968L286.116,407.958ZM364.147,407.958L312.126,407.958L312.126,433.968L364.147,433.968L364.147,407.958ZM214.587,303.916C214.587,286.08 199.91,271.403 182.074,271.403C164.238,271.403 149.561,286.08 149.561,303.916C149.561,321.752 164.238,336.429 182.074,336.429C182.075,336.429 182.075,336.429 182.076,336.429C199.911,336.429 214.587,321.753 214.587,303.918C214.587,303.917 214.587,303.917 214.587,303.916ZM370.65,303.916C370.65,286.08 355.973,271.403 338.137,271.403C320.301,271.403 305.624,286.08 305.624,303.916C305.624,321.752 320.301,336.429 338.137,336.429C338.138,336.429 338.139,336.429 338.139,336.429C355.974,336.429 370.65,321.753 370.65,303.918C370.65,303.917 370.65,303.917 370.65,303.916Z" style="fill-rule:nonzero;"/> <g transform="matrix(14.135,0,0,14.135,329.029,-28.2701)"> <path d="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2ZM17.19,15.94C17.15,16.03 17.1,16.11 17.03,16.18L15.34,17.87C15.19,18.02 15,18.09 14.81,18.09C14.62,18.09 14.43,18.02 14.28,17.87C13.99,17.58 13.99,17.1 14.28,16.81L14.69,16.4L9.1,16.4C7.8,16.4 6.75,15.34 6.75,14.05L6.75,12.28C6.75,11.87 7.09,11.53 7.5,11.53C7.91,11.53 8.25,11.87 8.25,12.28L8.25,14.05C8.25,14.52 8.63,14.9 9.1,14.9L14.69,14.9L14.28,14.49C13.99,14.2 13.99,13.72 14.28,13.43C14.57,13.14 15.05,13.14 15.34,13.43L17.03,15.12C17.1,15.19 17.15,15.27 17.19,15.36C17.27,15.55 17.27,15.76 17.19,15.94ZM17.25,11.72C17.25,12.13 16.91,12.47 16.5,12.47C16.09,12.47 15.75,12.13 15.75,11.72L15.75,9.95C15.75,9.48 15.37,9.1 14.9,9.1L9.31,9.1L9.72,9.5C10.01,9.79 10.01,10.27 9.72,10.56C9.57,10.71 9.38,10.78 9.19,10.78C9,10.78 8.81,10.71 8.66,10.56L6.97,8.87C6.9,8.8 6.85,8.72 6.81,8.63C6.73,8.45 6.73,8.24 6.81,8.06C6.85,7.97 6.9,7.88 6.97,7.81L8.66,6.12C8.95,5.83 9.43,5.83 9.72,6.12C10.01,6.41 10.01,6.89 9.72,7.18L9.31,7.59L14.9,7.59C16.2,7.59 17.25,8.65 17.25,9.94L17.25,11.72Z" style="fill-rule:nonzero;"/> </g></svg>`; const cogIco = `<svg style="width: 23px; fill: rgb(141, 145, 145);" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"viewBox="0 0 38.297 38.297" \t xml:space="preserve"> <g> \t<path d="M25.311,18.136l2.039-2.041l-2.492-2.492l-2.039,2.041c-1.355-0.98-2.941-1.654-4.664-1.934v-2.882H14.63v2.883 \t\tc-1.722,0.278-3.308,0.953-4.662,1.934l-2.041-2.041l-2.492,2.492l2.041,2.041c-0.98,1.354-1.656,2.941-1.937,4.662H2.658v3.523 \t\tH5.54c0.279,1.723,0.955,3.309,1.937,4.664l-2.041,2.039l2.492,2.492l2.041-2.039c1.354,0.979,2.94,1.653,4.662,1.936v2.883h3.524 \t\tv-2.883c1.723-0.279,3.309-0.955,4.664-1.936l2.039,2.039l2.492-2.492l-2.039-2.039c0.98-1.355,1.654-2.941,1.934-4.664h2.885 \t\tv-3.524h-2.885C26.967,21.078,26.293,19.492,25.311,18.136z M16.393,30.869c-3.479,0-6.309-2.83-6.309-6.307 \t\tc0-3.479,2.83-6.308,6.309-6.308c3.479,0,6.307,2.828,6.307,6.308C22.699,28.039,19.871,30.869,16.393,30.869z M35.639,8.113v-2.35 \t\th-0.965c-0.16-0.809-0.474-1.561-0.918-2.221l0.682-0.683l-1.664-1.66l-0.68,0.683c-0.658-0.445-1.41-0.76-2.217-0.918V0h-2.351 \t\tv0.965c-0.81,0.158-1.562,0.473-2.219,0.918L24.625,1.2l-1.662,1.66l0.683,0.683c-0.445,0.66-0.761,1.412-0.918,2.221h-0.966v2.35 \t\th0.966c0.157,0.807,0.473,1.559,0.918,2.217l-0.681,0.68l1.658,1.664l0.685-0.682c0.657,0.443,1.409,0.758,2.219,0.916v0.967h2.351 \t\tv-0.968c0.807-0.158,1.559-0.473,2.217-0.916l0.682,0.68l1.662-1.66l-0.682-0.682c0.444-0.658,0.758-1.41,0.918-2.217H35.639 \t\tL35.639,8.113z M28.701,10.677c-2.062,0-3.74-1.678-3.74-3.74c0-2.064,1.679-3.742,3.74-3.742c2.064,0,3.742,1.678,3.742,3.742 \t\tC32.443,9,30.766,10.677,28.701,10.677z"/> </g> </svg>`; const perplexityHelperModalId = 'perplexityHelperModal'; const getPerplexityHelperModal = () => $i(perplexityHelperModalId); const modalHTML = ` <div id="${perplexityHelperModalId}" class="modal"> <div class="modal-content"> <span class="close">×</span> <h1>Perplexity Helper settings</h1> <hr> </div> </div> `; const genCssName = x => `perplexity-helper--${x}`; const tagsContainerCls = genCssName('tags-container'); const threadTagContainerCls = genCssName('thread-tag-container'); const newTagContainerCls = genCssName('new-tag-container'); const newTagContainerInCollectionCls = genCssName('new-tag-container-in-collection'); const tagCls = genCssName('tag'); const tagDarkTextCls = genCssName('tag-dark-text'); const tagIconCls = genCssName('tag-icon'); const tagPaletteCls = genCssName('tag-palette'); const tagPaletteItemCls = genCssName('tag-palette-item'); const tagTweakNoBorderCls = genCssName('tag-tweak-no-border'); const tagTweakSlimPaddingCls = genCssName('tag-tweak-slim-padding'); const tagsPreviewCls = genCssName('tags-preview'); const tagsPreviewNewCls = genCssName('tags-preview-new'); const tagsPreviewThreadCls = genCssName('tags-preview-thread'); const tagTweakTextShadowCls = genCssName('tag-tweak-text-shadow'); const helpTextCls = genCssName('help-text'); const queryBoxCls = genCssName('query-box'); const controlsAreaCls = genCssName('controls-area'); const textAreaCls = genCssName('text-area'); const standardButtonCls = genCssName('standard-button'); const roundedMD = genCssName('rounded-md'); const topSettingsButtonId = genCssName('settings-button-top'); const leftSettingsButtonId = genCssName('settings-button-left'); const leftSettingsButtonWrapperId = genCssName('settings-button-left-wrapper'); const styles = ` .checkbox_label { color: white; } .textarea_wrapper { display: flex; flex-direction: column; } .textarea_wrapper > textarea { width: 100%; background-color: rgba(0, 0, 0, 0.8); padding: 0 5px; } .textarea_label { margin-right: auto; } .${helpTextCls} { max-width: 580px; background-color: #225; padding: 0.3em 0.7em; border-radius: 0.5em; margin: 1em 0; } .${helpTextCls} { cursor: text; } .${helpTextCls} code { font-size: 80%; background-color: rgba(255, 255, 255, 0.1); border-radius: 0.3em; padding: 0.1em; } .${helpTextCls} pre > code { background: none; } .${helpTextCls} pre { font-size: 80%; overflow: auto; background-color: rgba(255, 255, 255, 0.1); border-radius: 0.3em; padding: 0.1em 1em; } .${helpTextCls} li { list-style: circle; margin-left: 1em; } .${helpTextCls} hr { margin: 1em 0 0.5em 0; border-color: rgba(255, 255, 255, 0.1); } .btn-helper { margin-left: 20px } .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0, 0, 0, 0.8) } .modal-content { display: flex; margin: 1em auto; width: calc(100vw - 2em); padding: 20px; border: 1px solid #888; background-color: #202025; border-radius: 6px; color: rgb(206, 206, 210); flex-direction: column; position: relative; row-gap: 10px; overflow-y: auto; cursor: default; } .modal-content label { padding-right: 10px; } .modal-content h1 { font-size: 1.5em; } .modal-content hr { height: 1px; margin: 1em 0; border-color: rgba(255, 255, 255, 0.1); } .modal-content h1 + hr { margin-top: 0.5em; } .close { color: rgb(206, 206, 210); float: right; font-size: 28px; font-weight: bold; position: absolute; right: 20px; top: 5px; } .close:hover, .close:focus { color: white; text-decoration: none; cursor: pointer; } #copied-modal,#copied-modal-2 { padding: 5px 5px; background:gray; position:absolute; display: none; color: white; font-size: 15px; } label > div.select-none { user-select: text; cursor: initial; } .${tagsContainerCls} { display: flex; gap: 5px; margin: 5px 0; flex-wrap: wrap; } .${tagsContainerCls}.${threadTagContainerCls} { margin-left: 0.5em; margin-right: 0.5em; margin-bottom: 2px; } .${tagCls} { border: 1px solid #3b3b3b; background-color: #282828; /*color: rgba(255, 255, 255, 0.482);*/ /* equivalent of #909090; when on #282828 background */ padding: 0px 8px 0 8px; border-radius: 4px; cursor: pointer; transition: background-color 0.2s, color 0.2s; display: inline-block; color: #D0D0D0; } .${tagCls}.${tagDarkTextCls} { color: #333333; } .${tagCls}.${tagTweakNoBorderCls} { border: none; } .${tagCls}.${tagTweakSlimPaddingCls} { padding: 0px 4px 0 4px; } .${tagCls} .${tagIconCls} { width: 16px; height: 16px; margin-right: 2px; margin-left: -4px; margin-top: -4px; vertical-align: middle; display: inline-block; filter: invert(1); } .${tagCls}.${tagDarkTextCls} .${tagIconCls} { filter: none; } .${tagCls}.${tagTweakSlimPaddingCls} .${tagIconCls} { margin-left: -2px; } .${tagCls} span { position: relative; top: 1.5px; } .${tagCls}.${tagTweakTextShadowCls} span { text-shadow: 1px 0 0.5px black, -1px 0 0.5px black, 0 1px 0.5px black, 0 -1px 0.5px black; } .${tagCls}.${tagTweakTextShadowCls}.${tagDarkTextCls} span { text-shadow: 1px 0 0.5px white, -1px 0 0.5px white, 0 1px 0.5px white, 0 -1px 0.5px white; } .${tagCls}:hover { background-color: #333; color: #fff; } .${tagCls}.${tagDarkTextCls}:hover { /* color: #171717; */ color: #2f2f2f; } .${tagPaletteCls} { display: flex; flex-wrap: wrap; gap: 1px; } .${tagPaletteCls} .${tagPaletteItemCls} { text-shadow: 0 0 4px black; width: 35px; height: 20px; display: inline-block; text-align: center; border-radius: 0.2em; padding: 0 2px; transition: color 0.2s; } .${tagPaletteItemCls}:hover { cursor: pointer; color: white; } .${tagsPreviewCls} { background-color: #191a1a; padding: 0.5em 1em; border-radius: 1em; } .${tagsPreviewNewCls}:before { content: 'Target New: '; } .${tagsPreviewThreadCls}:before { content: 'Target Thread: '; } .${queryBoxCls} { flex-wrap: wrap; } .${controlsAreaCls} { grid-template-columns: repeat(4,minmax(0,1fr)) } .${textAreaCls} { grid-column-end: 5; } .${standardButtonCls} { grid-column-start: 4; } .${roundedMD} { border-radius: 0.375rem!important; } #${leftSettingsButtonId} svg { transition: fill 0.2s; } #${leftSettingsButtonId}:hover svg { fill: #fff !important; } .w-collapsedSideBarWidth #${leftSettingsButtonId} span { display: none; } .w-collapsedSideBarWidth #${leftSettingsButtonId} { width: 100%; border-radius: 0.25rem; height: 40px; } #${leftSettingsButtonWrapperId} { display: flex; padding: 0.1em 0.4em; justify-content: flex-end; } .w-collapsedSideBarWidth #${leftSettingsButtonWrapperId} { justify-content: center; } `; const TAG_POSITION = { BEFORE: 'before', AFTER: 'after', CARET: 'caret', }; const TAG_CONTAINER_TYPE = { NEW: 'new', NEW_IN_COLLECTION: 'new-in-collection', THREAD: 'thread', ALL: 'all', } const tagsHelpText = ` Each line is one tag. Non-field text is what will be inserted into prompt. Field is denoted by \`<\` and \`>\`, field name is before \`:\`, field value after \`:\`. Supported fields: - \`label\`: tag label shown on tag "box" (new items around prompt input area) - \`position\`: where the tag text will be inserted, default is \`before\`; valid values are \`before\`/\`after\` (existing text) or \`caret\` (at cursor position) - \`color\`: tag color; CSS colors supported, you can use colors from a pre-generated palette via \`%\` syntax, e.g. \`<color:%5>\`. See palette bellow. - \`tooltip\`: shown on hover (aka title); (default) tooltip can be disabled when this field is set to empty string - \`<tooltip:>\` - \`target\`: where the tag will be inserted, default is \`new\`; valid values are \`new\` (on home page or when clicking on "New Thread" button) / \`thread\` (on thread page) / \`all\` (everywhere) - \`hide\`: hide the tag from the tag list - \`link\`: link to a URL, e.g. \`<link:https://example.com>\`, can be used for collections. only one link per tag is supported. - \`link-target\`: target of the link, e.g. \`<link-target:_blank>\` (opens in new tab), default is \`_self\` (same tab). - \`icon\`: lucide icon name, e.g. \`<icon:arrow-right>\`. see [lucide icons](https://lucide.dev/icons) --- Examples: \`\`\` Vintage Story - stable diffusion web ui - <label:SDWU> , prefer concise modern syntax and style, <position:caret><label:concise modern> FFXIV: <color:%15><label:FFXIV> tell me a joke<label:Joke><tooltip:> \`\`\` `.trim(); const defaultTagColor = '#282828'; const changeValueUsingEvent = (selector, value) => { debugLog('changeValueUsingEvent', value, selector); const nativeTextareaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set; nativeTextareaValueSetter.call(selector, value); const inputEvent = new Event('input', {bubbles: true}); selector.dispatchEvent(inputEvent); } const cyanButtonPerplexityColor = '#1fb8cd'; const TAGS_PALETTE_COLORS_NUM = 16; const TAGS_PALETTE_CLASSIC = Object.freeze((() => { const step = 360 / TAGS_PALETTE_COLORS_NUM; const [startH, startS, startL, startA] = color2k.parseToHsla(cyanButtonPerplexityColor); return _.flow( _.map(x => startH + x * step, _), _.map(h => color2k.hsla(h, startS, startL, startA), _), _.sortBy(x => color2k.parseToHsla(x)[0], _) )(_.range(0, TAGS_PALETTE_COLORS_NUM)); })()); const TAGS_PALETTE_PASTEL = Object.freeze((() => { const step = 360 / TAGS_PALETTE_COLORS_NUM; const [startH, startS, startL, startA] = color2k.parseToHsla(cyanButtonPerplexityColor); return _.flow( _.map(x => startH + x * step, _), _.map(h => color2k.hsla(h, startS - 0.1, startL + 0.2, startA), _), _.sortBy(x => color2k.parseToHsla(x)[0], _) )(_.range(0, TAGS_PALETTE_COLORS_NUM)); })()); const TAGS_PALETTE_GRIM = Object.freeze((() => { const step = 360 / TAGS_PALETTE_COLORS_NUM; const [startH, startS, startL, startA] = color2k.parseToHsla(cyanButtonPerplexityColor); return _.flow( _.map(x => startH + x * step, _), _.map(h => color2k.hsla(h, startS - 0.6, startL - 0.3, startA), _), _.sortBy(x => color2k.parseToHsla(x)[0], _) )(_.range(0, TAGS_PALETTE_COLORS_NUM)); })()); const TAGS_PALETTE_DARK = Object.freeze((() => { const step = 360 / TAGS_PALETTE_COLORS_NUM; const [startH, startS, startL, startA] = color2k.parseToHsla(cyanButtonPerplexityColor); return _.flow( _.map(x => startH + x * step, _), _.map(h => color2k.hsla(h, startS, startL - 0.4, startA), _), _.sortBy(x => color2k.parseToHsla(x)[0], _) )(_.range(0, TAGS_PALETTE_COLORS_NUM)); })()); const TAGS_PALETTE_GRAY = Object.freeze((() => { const step = 1 / TAGS_PALETTE_COLORS_NUM; return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(0, 0, step * x, 1)); })()); const TAGS_PALETTE_CYAN = Object.freeze((() => { const step = 1 / TAGS_PALETTE_COLORS_NUM; const [startH, startS, startL, startA] = color2k.parseToHsla(cyanButtonPerplexityColor); return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(startH, startS, step * x, 1)); })()); const TAGS_PALETTE_TRANSPARENT = Object.freeze((() => { const step = 1 / TAGS_PALETTE_COLORS_NUM; return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(0, 0, 0, step * x)); })()); const TAGS_PALETTE_HACKER = Object.freeze((() => { const step = 1 / TAGS_PALETTE_COLORS_NUM; return _.range(0, TAGS_PALETTE_COLORS_NUM).map(x => color2k.hsla(120, step * x, step * x * 0.5, 1)); })()); const TAGS_PALETTES = Object.freeze({ CLASSIC: TAGS_PALETTE_CLASSIC, PASTEL: TAGS_PALETTE_PASTEL, GRIM: TAGS_PALETTE_GRIM, DARK: TAGS_PALETTE_DARK, GRAY: TAGS_PALETTE_GRAY, CYAN: TAGS_PALETTE_CYAN, TRANSPARENT: TAGS_PALETTE_TRANSPARENT, HACKER: TAGS_PALETTE_HACKER, }); const convertColorInPaletteFormat = currentPalette => value => currentPalette[parseInt(dropStr(1)(value), 10)] ?? defaultTagColor; const TAG_HOME_PAGE_LAYOUT = { DEFAULT: 'default', COMPACT: 'compact', WIDER: 'wider', WIDE: 'wide', } const processTagField = currentPalette => name => value => { if (name === 'color' && value.startsWith('%')) return convertColorInPaletteFormat(currentPalette)(value); if (name === 'hide') return true; return value; }; const tagLineRegex = /<(label|position|color|tooltip|target|hide|link|link-target|icon)(?::([^<>]*))?>/g; const parseOneTagLine = currentPalette => line => Array.from(line.matchAll(tagLineRegex)).reduce( (acc, match) => { const [fullMatch, field, value] = match; const processedValue = processTagField(currentPalette)(field)(value); return { ...acc, [field]: processedValue, text: acc.text.replace(fullMatch, '').replace(/\\n/g, '\n'), }; }, {text: line, color: defaultTagColor, target: TAG_CONTAINER_TYPE.NEW, hide: false, 'link-target': '_self'} ); const parseTagsText = text => { const lines = text.split('\n').filter(tag => tag.trim().length > 0); const palette = getPalette(loadConfig()?.tagPalette); return lines.map(parseOneTagLine(palette)); }; const getTagsContainer = () => $c(tagsContainerCls); const promptAreaOfNewThreadSelector = 'textarea[placeholder="Ask anything..."]'; const getPromptAreaOfNewThread = () => jq(promptAreaOfNewThreadSelector); const getPromptAreaWrapperOfNewThread = () => getPromptAreaOfNewThread().nthParent(5); const promptAreaOnThreadSelector = 'textarea[placeholder="Ask follow-up"]'; const getPromptAreaOnThread = () => jq(promptAreaOnThreadSelector); const getPromptAreaWrapperOnThread = () => getPromptAreaOnThread().parent().parent().parent().parent(); const promptAreaOnCollectionSelector = 'textarea[placeholder="New Thread"]'; const getPromptAreaOnCollection = () => jq(promptAreaOnCollectionSelector); const getPromptAreaWrapperOnCollection = () => getPromptAreaOnCollection().nthParent(4); const anyPromptAreaSelector = `${promptAreaOfNewThreadSelector},${promptAreaOnThreadSelector},${promptAreaOnCollectionSelector}`; const getAnyPromptArea = () => jq(anyPromptAreaSelector); const posFromTag = tag => Object.values(TAG_POSITION).includes(tag.position) ? tag.position : TAG_POSITION.BEFORE; const applyTagToString = (tag, val, caretPos) => { const {text} = tag; switch (posFromTag(tag)) { case TAG_POSITION.BEFORE: return `${text}${val}`; case TAG_POSITION.AFTER: return `${val}${text}`; case TAG_POSITION.CARET: return `${takeStr(caretPos)(val)}${text}${dropStr(caretPos)(val)}`; default: throw new Error(`Invalid position: ${tag.position}`); } }; const getPromptAreaFromTagsContainer = tagsContainerEl => tagsContainerEl.parent().find(anyPromptAreaSelector); const getPalette = paletteName => TAGS_PALETTES[paletteName] ?? TAGS_PALETTES.CLASSIC; const createTag = containerEl => isPreview => tag => { if (tag.hide) return null; const labelString = tag.label ?? tag.text; const isTagLight = color2k.getLuminance(tag.color) > 0.35; const colorMod = isTagLight ? color2k.darken : color2k.lighten; const hoverBgColor = color2k.toRgba(colorMod(tag.color, 0.1)); const borderColor = color2k.toRgba(colorMod(tag.color, loadConfig().tagTweakRichBorderColor ? 0.2 : 0.1)); const clickHandler = evt => { debugLog('clicked', tag, evt); if (tag.link) return; const el = jq(evt.currentTarget); const promptArea = getPromptAreaFromTagsContainer(el.parent()); if (!promptArea.length) { debugLogTags('no prompt area found', promptArea); return; } const promptAreaRaw = promptArea[0]; const newText = applyTagToString(tag, promptArea.val(), promptAreaRaw.selectionStart); changeValueUsingEvent(promptAreaRaw, newText); promptAreaRaw.focus(); }; const tagFont = loadConfig().tagFont; const defaultTooltip = tag.link? `${logPrefix} Open link: ${tag.link}` : `${logPrefix} Insert \`${tag.text}\` at position \`${posFromTag(tag)}\``; const tagEl = jq(`<div/>`) .addClass(tagCls) .prop('title', tag.tooltip ?? defaultTooltip) .attr('data-tag', JSON.stringify(tag)) .css({ backgroundColor: tag.color, borderColor, fontFamily: tagFont, }) .attr('data-color', color2k.toHex(tag.color)) .attr('data-hoverBgColor', color2k.toHex(hoverBgColor)) .attr('data-font', tagFont) .on('mouseenter', event => { jq(event.currentTarget).css('background-color', hoverBgColor); }) .on('mouseleave', event => { jq(event.currentTarget).css('background-color', tag.color); }); if (isTagLight) { tagEl.addClass(tagDarkTextCls); } if (loadConfig()?.tagTweakNoBorder) { tagEl.addClass(tagTweakNoBorderCls); } if (loadConfig()?.tagTweakSlimPadding) { tagEl.addClass(tagTweakSlimPaddingCls); } if (loadConfig()?.tagTweakTextShadow) { tagEl.addClass(tagTweakTextShadowCls); } const textEl = jq('<span/>').text(labelString); if (tag.icon) { const iconEl = jq('<img/>') .attr('src', `https://unpkg.com/lucide-static@latest/icons/${tag.icon}.svg`) .addClass(tagIconCls); textEl.prepend(iconEl); } tagEl.append(textEl); if (tag.link) { const linkEl = jq('<a/>') .attr('href', tag.link) .attr('target', tag['link-target']) .css({ textDecoration: 'none', color: 'inherit' }) .append(tagEl); if (!isPreview) { linkEl.on('click', e => { e.preventDefault(); if (tag['link-target'] === '_blank') { window.open(tag.link, '_blank'); } else { window.location.href = tag.link; } }); } containerEl.append(linkEl); } else { if (!isPreview) { tagEl.click(clickHandler); } containerEl.append(tagEl); } return tagEl; }; const genDebugFakeTags = () => _.times(TAGS_PALETTE_COLORS_NUM, x => `Fake ${x} ${_.times(x / 3).map(() => 'x').join('')}<color:%${x % TAGS_PALETTE_COLORS_NUM}>`) .join('\n'); const getTagContainerType = containerEl => { if (containerEl.hasClass(threadTagContainerCls) || containerEl.hasClass(tagsPreviewThreadCls)) return TAG_CONTAINER_TYPE.THREAD; if (containerEl.hasClass(newTagContainerCls) || containerEl.hasClass(tagsPreviewNewCls)) return TAG_CONTAINER_TYPE.NEW; if (containerEl.hasClass(newTagContainerInCollectionCls) || containerEl.hasClass(tagsPreviewNewInCollectionCls)) return TAG_CONTAINER_TYPE.NEW_IN_COLLECTION; return null; } const isTagRelevantForContainer = containerType => tag => containerType === tag.target || (containerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION && tag.target === TAG_CONTAINER_TYPE.NEW) || tag.target === TAG_CONTAINER_TYPE.ALL const refreshTags = ({force = false} = {}) => { const promptWrapper = getPromptAreaWrapperOfNewThread().add(getPromptAreaWrapperOnThread()).add(getPromptAreaWrapperOnCollection()); if (!promptWrapper.length) { debugLogTags('no prompt area found'); } const allTags = _.flow( x => x + (unsafeWindow.phFakeTags ? `${nl}${genDebugFakeTags()}${nl}` : ''), parseTagsText, )(loadConfig()?.tagsText ?? defaultConfig.tagsText); debugLogTags('refreshing allTags', allTags); const createContainer = (promptWrapper) => { const el = jq(`<div/>`).addClass(tagsContainerCls); if (promptWrapper.find(promptAreaOnThreadSelector).length) { el.addClass(threadTagContainerCls); } if (promptWrapper.find(promptAreaOfNewThreadSelector).length) { el.addClass(newTagContainerCls); } if (promptWrapper.find(promptAreaOnCollectionSelector).length) { el.addClass(newTagContainerInCollectionCls); } return el; } promptWrapper.each((_, rEl) => { const el = jq(rEl); if (el.parent().find(`.${tagsContainerCls}`).length) { el.parent().addClass(queryBoxCls); return; } el.before(createContainer(el)); }); const containerEls = getTagsContainer(); containerEls.each((_i, rEl) => { const containerEl = jq(rEl); const isPreview = Boolean(containerEl.attr('data-preview')); const currentTags = containerEl.find(`.${tagCls}`).map((i, el) => JSON.parse(el.dataset.tag)).toArray(); const tagContainerType = getTagContainerType(containerEl); const tagsForThisContainer = allTags.filter(isTagRelevantForContainer(tagContainerType)).filter(tag => !tag.hide); debugLogTags('tagContainerType', tagContainerType, 'current tags', currentTags, 'tagsForThisContainer', tagsForThisContainer); if (_.isEqual(currentTags, tagsForThisContainer) && !force) { debugLogTags('no tags changed'); return; } containerEl.empty(); const tagHomePageLayout = loadConfig()?.tagHomePageLayout; console.log('tagHomePageLayout', {tagHomePageLayout, tagContainerType, containerEl}); const applyCompact = () => { containerEl.css({'margin-top': '-2em', 'margin-bottom': '1px', 'gap': '1px'}); }; const applyWider = () => { containerEl.css({'margin-left': '-6em', 'margin-right': '-6em', 'margin-bottom': '2em'}); }; const applyWide = () => { containerEl.css({'margin-left': '-12em', 'margin-right': '-12em', 'margin-bottom': '3em'}); }; if (!isPreview) { if ((tagContainerType === TAG_CONTAINER_TYPE.NEW || tagContainerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION)) { if (tagContainerType === TAG_CONTAINER_TYPE.NEW_IN_COLLECTION) { // only compact layout is supported for new in collection if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.COMPACT) applyCompact(); } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.COMPACT) applyCompact(); else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.WIDER) applyWider(); else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.WIDE) applyWide(); else containerEl.css({'margin-left': '', 'margin-right': ''}); } } tagsForThisContainer.forEach(createTag(containerEl)(isPreview)); }); } const setupTags = () => { debugLog('setting up tags'); setInterval(refreshTags, 500); } const defaultConfig = Object.freeze({ showCopilot: true, showCopilotNewThread: true, showCopilotRepeatLast: true, showCopilotCopyPlaceholder: true, tagsText: '', debugMode: false, debugTagsMode: false, tagPalette: 'CLASSIC', tagHomePageLayout: TAG_HOME_PAGE_LAYOUT.DEFAULT, }); // TODO: if still using local storage, at least it should be prefixed with user script name const storageKey = 'checkBoxStates'; const loadConfig = () => { try { // TODO: use storage from GM API const val = JSON.parse(localStorage.getItem(storageKey)); // debugLog('loaded config', val); return val; } catch (e) { console.error('Failed to load config, using default', e); return defaultConfig; } } const loadConfigOrDefault = () => loadConfig() ?? defaultConfig const saveConfig = cfg => { debugLog('saving config', cfg); localStorage.setItem(storageKey, JSON.stringify(cfg)); }; const createCheckbox = (id, labelText, onChange) => { debugLog("createCheckbox", id); const checkbox = jq(`<input type="checkbox" id=${id}>`); const label = jq(`<label class="checkbox_label" for="${id}">${labelText}</label>`); const checkboxWithLabel = jq('<div class="checkbox_wrapper"></div>').append(label).append(checkbox); debugLog('checkboxwithlabel', checkboxWithLabel); getSettingsModalContent().append(checkboxWithLabel); checkbox.on('change', onChange); return checkbox; }; const createTextArea = (id, labelText, onChange, helpText) => { debugLog("createTextArea", id); const textarea = jq(`<textarea id=${id}></textarea>`); const label = jq(`<label class="textarea_label">${labelText}${helpText ? ' 📖' : ''}</label>`); const textareaWithLabel = jq('<div class="textarea_wrapper"></div>').append(label); if (helpText) { const help = jq(`<div/>`).addClass(helpTextCls).html(markdownConverter.makeHtml(helpText)).append(jq('<br/>')); help.append(jq('<button/>').text('[Close help]').on('click', () => help.hide())); textareaWithLabel.append(help); label .css({cursor: 'pointer'}) .on('click', () => help.toggle()) .prop('title', 'Click to toggle help') ; help.hide(); } textareaWithLabel.append(textarea); debugLog('textareaWithLabel', textareaWithLabel); getSettingsModalContent().append(textareaWithLabel); textarea.on('change', onChange); return textarea; }; const createSelect = (id, labelText, options, onChange) => { const select = jq(`<select id=${id}>`); options.forEach(({value, label}) => { jq('<option>').val(value).text(label).appendTo(select); }); const label = jq(`<label class="select_label">${labelText}</label>`); const selectWithLabel = jq('<div class="select_wrapper"></div>').append(label).append(select); debugLog('selectWithLabel', selectWithLabel); getSettingsModalContent().append(selectWithLabel); select.on('change', onChange); return select; }; const createPaletteLegend = paletteName => { const wrapper = jq('<div/>') .addClass(tagPaletteCls) .append(jq('<span>').html('Palette of color codes: ')) ; const palette = getPalette(paletteName); palette.forEach((color, i) => { const colorCode = `%${i}`; const colorPart = genColorPart(colorCode); console.log('createPaletteLegend', {i, colorCode, colorPart, color}); jq('<span/>') .text(colorCode) .addClass(tagPaletteItemCls) .css({ 'background-color': color, }) .prop('title', `Copy ${colorPart} to clipboard`) .click(() => { copyTextToClipboard(colorPart); }) .appendTo(wrapper); }); return wrapper; } const createColorInput = (id, labelText, onChange) => { debugLog("createColorInput", id); const input = jq(`<input type="color" id=${id}>`); const label = jq(`<label class="color_label">${labelText}</label>`); const inputWithLabel = jq('<div class="color_wrapper"></div>').append(label).append(input); debugLog('inputWithLabel', inputWithLabel); getSettingsModalContent().append(inputWithLabel); input.on('change', onChange); return input; } const createTagsPreview = () => { const wrapper = jq('<div/>') .addClass(tagsPreviewCls) .append(jq('<div>').html('Preview')) .append(jq('<div>').addClass(tagsPreviewNewCls).addClass(tagsContainerCls).attr('data-preview', 'true')) .append(jq('<div>').addClass(tagsPreviewThreadCls).addClass(tagsContainerCls).attr('data-preview', 'true')) ; getSettingsModalContent().append(wrapper); } const coPilotNewThreadAutoSubmitCheckboxId = 'coPilotNewThreadAutoSubmit'; const getCoPilotNewThreadAutoSubmitCheckbox = () => $i(coPilotNewThreadAutoSubmitCheckboxId); const coPilotRepeatLastAutoSubmitCheckboxId = 'coPilotRepeatLastAutoSubmit'; const getCoPilotRepeatLastAutoSubmitCheckbox = () => $i(coPilotRepeatLastAutoSubmitCheckboxId); const hideSideMenuCheckboxId = 'hideSideMenu'; const getHideSideMenuCheckbox = () => $i(hideSideMenuCheckboxId); const tagsTextAreaId = 'tagsText'; const getTagsTextArea = () => $i(tagsTextAreaId); const tagColorPickerId = genCssName('tagColorPicker'); const getTagColorPicker = () => $i(tagColorPickerId); const enableDebugCheckboxId = genCssName('enableDebug'); const getEnableDebugCheckbox = () => $i(enableDebugCheckboxId); const enableTagsDebugCheckboxId = genCssName('enableTagsDebug'); const getEnableTagsDebugCheckbox = () => $i(enableTagsDebugCheckboxId); const tagPaletteSelectId = genCssName('tagPaletteSelect'); const getTagPaletteSelect = () => $i(tagPaletteSelectId); const tagFontSelectId = genCssName('tagFontSelect'); const getTagFontSelect = () => $i(tagFontSelectId); const tagTweakNoBorderCheckboxId = genCssName('tagTweakNoBorder'); const getTagTweakNoBorderCheckbox = () => $i(tagTweakNoBorderCheckboxId); const tagTweakSlimPaddingCheckboxId = genCssName('tagTweakSlimPadding'); const getTagTweakSlimPaddingCheckbox = () => $i(tagTweakSlimPaddingCheckboxId); const tagTweakRichBorderColorCheckboxId = genCssName('tagTweakRichBorderColor'); const getTagTweakRichBorderColorCheckbox = () => $i(tagTweakRichBorderColorCheckboxId); const tagTweakTextShadowCheckboxId = genCssName('tagTweakTextShadow'); const getTagTweakTextShadowCheckbox = () => $i(tagTweakTextShadowCheckboxId); const tagHomePageLayoutSelectId = genCssName('tagHomePageLayout'); const getTagHomePageLayoutSelect = () => $i(tagHomePageLayoutSelectId); const copyTextToClipboard = async text => { try { await navigator.clipboard.writeText(text); console.log('Text copied to clipboard', {text}); } catch (err) { console.error('Failed to copy text: ', err); } }; const genColorPart = color => `<color:${color}>`; function handleSettingsInit() { const modalExists = getPerplexityHelperModal().length > 0; const firstCheckboxExists = getCoPilotNewThreadAutoSubmitCheckbox().length > 0; if (!modalExists || firstCheckboxExists) { return; } const insertSeparator = () => getSettingsModalContent().append('<hr/>'); createCheckbox(coPilotNewThreadAutoSubmitCheckboxId, 'Auto Submit New Thread With CoPilot', saveConfigFromForm); createCheckbox(coPilotRepeatLastAutoSubmitCheckboxId, 'Auto Submit Repeat With CoPilot', saveConfigFromForm); createCheckbox(hideSideMenuCheckboxId, 'Hide Side Menu', saveConfigFromForm); insertSeparator(); createTextArea(tagsTextAreaId, 'Tags', saveConfigFromForm, tagsHelpText) .prop('rows', 8).css('min-width', '700px').prop('wrap', 'off'); const paletteLegendContainer = jq('<div/>').attr('id', 'palette-legend-container'); getSettingsModalContent().append(paletteLegendContainer); const updatePaletteLegend = () => { paletteLegendContainer.empty().append(createPaletteLegend(loadConfig()?.tagPalette)); }; updatePaletteLegend(); createSelect( tagPaletteSelectId, 'Tag color palette:', Object.entries(TAGS_PALETTES).map(([key, value]) => ({value: key, label: key})), () => { saveConfigFromForm(); updatePaletteLegend(); refreshTags(); } ); const FONTS = Object.keys(fontUrls); createSelect( tagFontSelectId, 'Tag font:', FONTS.map(font => ({ value: font, label: font })), () => { saveConfigFromForm(); loadFont(loadConfigOrDefault().tagFont); refreshTags({force: true}); } ); createColorInput(tagColorPickerId, 'Custom color - copy field for tag to clipboard:', () => { const color = getTagColorPicker().val(); debugLog('color', color); copyTextToClipboard(genColorPart(color)); }); const saveConfigFromFormAndForceRefresh = () => { saveConfigFromForm(); refreshTags({force: true}); }; createCheckbox(tagTweakNoBorderCheckboxId, 'No border', saveConfigFromFormAndForceRefresh); createCheckbox(tagTweakSlimPaddingCheckboxId, 'Slim padding', saveConfigFromFormAndForceRefresh); createCheckbox(tagTweakRichBorderColorCheckboxId, 'Rich Border Color', saveConfigFromFormAndForceRefresh); createCheckbox(tagTweakTextShadowCheckboxId, 'Text shadow', saveConfigFromFormAndForceRefresh); createSelect( tagHomePageLayoutSelectId, 'Tag container layout on home page:', Object.values(TAG_HOME_PAGE_LAYOUT).map(value => ({value, label: value})), saveConfigFromForm ); createTagsPreview(); insertSeparator(); // debug options at the bottom createCheckbox(enableDebugCheckboxId, 'Enable Debug', saveConfigFromForm); createCheckbox(enableTagsDebugCheckboxId, 'Enable tags debug log', saveConfigFromForm); const savedStates = JSON.parse(localStorage.getItem(storageKey)); if (savedStates === null) { return; } getCoPilotNewThreadAutoSubmitCheckbox().prop('checked', savedStates.coPilotNewThreadAutoSubmit); getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked', savedStates.coPilotRepeatLastAutoSubmit); getHideSideMenuCheckbox().prop('checked', savedStates.hideSideMenu); getTagsTextArea().val(savedStates.tagsText); getEnableDebugCheckbox().prop('checked', savedStates.enableDebug); getEnableTagsDebugCheckbox().prop('checked', savedStates.debugTagsMode); getTagPaletteSelect().val(savedStates.tagPalette); getTagFontSelect().val(savedStates.tagFont); getTagTweakNoBorderCheckbox().prop('checked', savedStates.tagTweakNoBorder); getTagTweakSlimPaddingCheckbox().prop('checked', savedStates.tagTweakSlimPadding); getTagTweakRichBorderColorCheckbox().prop('checked', savedStates.tagTweakRichBorderColor); getTagTweakTextShadowCheckbox().prop('checked', savedStates.tagTweakTextShadow); getTagHomePageLayoutSelect().val(savedStates.tagHomePageLayout); } debugLog(jq.fn.jquery); const getSavedStates = () => JSON.parse(localStorage.getItem(storageKey)); const getModal = () => jq("[data-testid='quick-search-modal'] > div"); const getCopilotToggleButton = textarea => textarea.parent().parent().find('[data-testid="copilot-toggle"]'); const upperControls = () => jq('svg[data-icon="lock"] ~ div:contains("Share")').nthParent(5).closest('.flex.justify-between:not(.grid-cols-3)'); const getControlsArea = () => jq('textarea[placeholder="Ask follow-up"]').parent().parent().children().last(); const getCopilotNewThreadButton = () => jq('#copilot_new_thread'); const getCopilotRepeatLastButton = () => jq('#copilot_repeat_last'); const getSelectAllButton = () => jq('#perplexity_helper_select_all'); const getSelectAllAndSubmitButton = () => jq('#perplexity_helper_select_all_and_submit'); const getCopyPlaceholder = () => jq('#perplexity_helper_copy_placeholder'); const getCopyAndFillInPlaceholder = () => jq('#perplexity_helper_copy_placeholder_and_fill_in'); const getTopSettingsButtonEl = () => $i(topSettingsButtonId); const getLeftSettingsButtonEl = () => $i(leftSettingsButtonId); const getSideMenu = () => jq('.min-h-\\[100vh\\]').children().first(); const getSettingsModalContent = () => getPerplexityHelperModal().find('.modal-content'); const getSubmitBtn0 = () => jq('svg[data-icon="arrow-up"]').last().parent().parent(); const getSubmitBtn1 = () => jq('svg[data-icon="arrow-right"]').last().parent().parent(); const getSubmitBtn2 = () => jq('svg[data-icon="code-fork"]').last().parent().parent(); const isStandardControlsAreaFc = () => !getControlsArea().hasClass('bottom-0'); const getCurrentControlsArea = () => isStandardControlsAreaFc() ? getControlsArea() : getControlsArea().find('.bottom-0'); const getDashedCheckboxButton = () => jq('svg[data-icon="square-dashed"]').parent().parent(); const getStarSVG = () => jq('svg[data-icon="star-christmas"]'); const getSpecifyQuestionBox = () => jq('svg[data-icon="star-christmas"]').parent().parent().parent().last(); const getNumberOfDashedSVGs = () => getSpecifyQuestionBox().find('svg[data-icon="square-dashed"]').length; const getSpecifyQuestionControlsWrapper = () => getSpecifyQuestionBox().find('button:contains("Continue")').parent() const getCopiedModal = () => jq('#copied-modal'); const getCopiedModal2 = () => jq('#copied-modal-2'); const getCopyPlaceholderInput = () => getSpecifyQuestionBox().find('textarea'); const getSubmitButton0or2 = () => getSubmitBtn0().length < 1 ? getSubmitBtn2() : getSubmitBtn0(); const questionBoxWithPlaceholderExists = () => getSpecifyQuestionBox().find('textarea')?.attr('placeholder')?.length > 0 ?? false; const selectAllCheckboxes = () => { const currentCheckboxes = getDashedCheckboxButton(); debugLog('checkboxes', currentCheckboxes); const removeLastObject = (arr) => { if (!_.isEmpty(arr)) { debugLog('arr', arr); const newArr = _.dropRight(arr, 1); debugLog("newArr", newArr); getDashedCheckboxButton().last().click(); return setTimeout(() => { removeLastObject(newArr) }, 1) } }; removeLastObject(currentCheckboxes); } const isCopilotOn = (el) => el.hasClass('text-super') const toggleBtnDot = (btnDot, value) => { debugLog(' toggleBtnDot btnDot', btnDot); const btnDotInner = btnDot.find('.rounded-full'); debugLog('btnDotInner', btnDotInner); if (!btnDotInner.hasClass('bg-super') && value === true) { btnDot.click(); } } const checkForCopilotToggleState = (timer, checkCondition, submitWhenTrue, submitButtonVersion) => { debugLog("checkForCopilotToggleState run", timer, checkCondition(), submitWhenTrue, submitButtonVersion); if (checkCondition()) { clearInterval(timer); debugLog("checkForCopilotToggleState condition met, interval cleared"); const submitBtn = submitButtonVersion === 0 ? getSubmitButton0or2() : getSubmitBtn1(); debugLog('submitBtn', submitBtn); if (submitWhenTrue) { submitBtn.click(); } } } const openNewThreadModal = (lastQuery) => { debugLog('openNewThreadModal', lastQuery) const newThreadText = jq(".sticky div").filter(function () { return /^New Thread$/i.test(jq(this).text()); }); if (!newThreadText.length) { debugLog('newThreadText.length should be 1', newThreadText.length); return; } debugLog('newThreadText', newThreadText); newThreadText.click(); setTimeout(() => { debugLog('newThreadText.click()'); const modal = getModal(); if (modal.length > 0) { const textArea = modal.find('textarea'); if (textArea.length !== 1) debugLog('textArea.length should be 1', textArea.length); const newTextArea = textArea.last(); const textareaElement = newTextArea[0]; debugLog('textareaElement', textareaElement); changeValueUsingEvent(textareaElement, lastQuery); const copilotButton = getCopilotToggleButton(newTextArea); toggleBtnDot(copilotButton, true); const isCopilotOnBtn = () => isCopilotOn(copilotButton); const coPilotNewThreadAutoSubmit = getSavedStates() ? getSavedStates().coPilotNewThreadAutoSubmit : getCoPilotNewThreadAutoSubmitCheckbox().prop('checked'); const copilotCheck = () => { const ctx = {timer: null}; ctx.timer = setInterval(() => checkForCopilotToggleState(ctx.timer, isCopilotOnBtn, coPilotNewThreadAutoSubmit, 1), 500); } copilotCheck(); } else { debugLog('else of modal.length > 0'); } }, 2000); } const getLastQuery = () => { // wrapper around prompt + response const lastQueryBox = jq('svg[data-icon="repeat"]').last().nthParent(7); if (lastQueryBox.length === 0) { debugLog('lastQueryBox not found'); } const wasCopilotUsed = lastQueryBox.find('svg[data-icon="star-christmas"]').length > 0; const lastQueryBoxText = lastQueryBox.find('.whitespace-pre-line').text(); debugLog('[getLastQuery]', {lastQueryBox, wasCopilotUsed, lastQueryBoxText}); return lastQueryBoxText ?? null; } const saveConfigFromForm = () => { const checkBoxStates = { coPilotNewThreadAutoSubmit: getCoPilotNewThreadAutoSubmitCheckbox().prop('checked'), coPilotRepeatLastAutoSubmit: getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked'), hideSideMenu: getHideSideMenuCheckbox().prop('checked'), tagsText: getTagsTextArea().val(), enableDebug: getEnableDebugCheckbox().prop('checked'), debugTagsMode: getEnableTagsDebugCheckbox().prop('checked'), tagPalette: getTagPaletteSelect().val(), tagFont: getTagFontSelect().val(), tagTweakNoBorder: getTagTweakNoBorderCheckbox().prop('checked'), tagTweakSlimPadding: getTagTweakSlimPaddingCheckbox().prop('checked'), tagTweakRichBorderColor: getTagTweakRichBorderColorCheckbox().prop('checked'), tagTweakTextShadow: getTagTweakTextShadowCheckbox().prop('checked'), tagHomePageLayout: getTagHomePageLayoutSelect().val(), }; saveConfig(checkBoxStates); }; const showPerplexityHelperModal = () => { getPerplexityHelperModal().show().css('display', 'flex'); } const hidePerplexityHelperModal = () => { getPerplexityHelperModal().hide(); } const handleTopSettingsButtonInsertion = () => { const copilotHelperSettings = getTopSettingsButtonEl(); debugLog('upperControls().length > 0', upperControls().length, 'copilotHelperSettings.length', copilotHelperSettings.length, 'upperControls().children().length', upperControls().children().length); if (upperControls().length > 0 && copilotHelperSettings.length < 1 && upperControls().children().length >= 1) { debugLog('inserting settings button'); upperControls().children().eq(0).children().eq(0).append(upperButton(topSettingsButtonId, cogIco, 'Perplexity Helper Settings')); } }; const handleTopSettingsButtonSetup = () => { const settingsButtonEl = getTopSettingsButtonEl(); if (settingsButtonEl.length === 1 && !settingsButtonEl.attr('data-has-custom-click-event')) { debugLog('handleTopSettingsButtonSetup: setting up the button'); if (settingsButtonEl.length === 0) { debugLog('handleTopSettingsButtonSetup: settingsButtonEl.length === 0'); } settingsButtonEl.on("click", () => { debugLog('perplexity_helper_settings open click'); showPerplexityHelperModal(); }); settingsButtonEl.attr('data-has-custom-click-event', true); } }; const applySideMenuHiding = () => { const sideMenu = getSideMenu(); if (getSavedStates()) getSavedStates().hideSideMenu || getHideSideMenuCheckbox().prop('checked') ? sideMenu.hide() : sideMenu.show(); }; const handleModalCreation = () => { if (getPerplexityHelperModal().length > 0) return; debugLog('handleModalCreation: creating modal'); jq("body").append(modalHTML); getPerplexityHelperModal().find('.close').on('click', () => { debugLog('perplexity_helper_settings close click'); hidePerplexityHelperModal(); }); }; const getLeftPanel = () => jq('.fixed svg[data-icon="library"]').nthParent(6 + 2); const handleLeftSettingsButtonSetup = () => { const existingLeftSettingsButton = getLeftSettingsButtonEl(); if (existingLeftSettingsButton.length === 1) { const wrapper = existingLeftSettingsButton.parent(); if (!wrapper.is(':last-child')) { wrapper.appendTo(wrapper.parent()); } return; } const leftPanel = getLeftPanel(); if (leftPanel.length === 0) { debugLog('handleLeftSettingsButtonSetup: leftPanel not found'); } const wrapperEl = jq('<div>').attr('id', leftSettingsButtonWrapperId); const iconEl = jq(cogIco); const btnEl = jq('<button>') .attr('id', leftSettingsButtonId) .attr('title', 'Perplexity Helper Settings') .addClass('text-textOff dark:text-textOffDark font-sans text-xs flex items-center gap-x-sm') .addClass('md:hover:bg-offsetPlus text-textOff dark:text-textOffDark md:hover:text-textMain dark:md:hover:bg-offsetPlusDark dark:md:hover:text-textMainDark font-sans focus:outline-none outline-none outline-transparent transition duration-300 ease-in-out font-sans select-none items-center relative group justify-center text-center items-center rounded-full cursor-point active:scale-95 origin-center whitespace-nowrap inline-flex text-sm px-sm font-medium h-8') .append(jq('<span>').text('Perplexity Helper')) .append(iconEl) ; btnEl.on('click', () => { debugLog('left settings button clicked'); showPerplexityHelperModal(); }); wrapperEl.append(btnEl); leftPanel.append(wrapperEl); } const work = () => { handleModalCreation(); handleTopSettingsButtonInsertion(); handleTopSettingsButtonSetup(); handleSettingsInit(); handleLeftSettingsButtonSetup(); applySideMenuHiding(); const regex = /^https:\/\/www\.perplexity\.ai\/search\/?.*/; const currentUrl = jq(location).attr('href'); const matchedCurrentUrlAsSearchPage = regex.test(currentUrl); // debugLog("currentUrl", currentUrl); // debugLog("matchedCurrentUrlAsSearchPage", matchedCurrentUrlAsSearchPage); if (matchedCurrentUrlAsSearchPage) { const controlsArea = getCurrentControlsArea(); controlsArea.addClass(controlsAreaCls); controlsArea.parent().find('textarea').first().addClass(textAreaCls); controlsArea.addClass(roundedMD); controlsArea.parent().addClass(roundedMD); if (controlsArea.length === 0) { debugLog('controlsArea not found', { controlsArea, currentControlsArea: getCurrentControlsArea(), isStandardControlsAreaFc: isStandardControlsAreaFc() }); } const lastQueryBoxText = getLastQuery(); const mainTextArea = isStandardControlsAreaFc() ? controlsArea.prev().prev() : controlsArea.parent().prev(); if (mainTextArea.length === 0) { debugLog('mainTextArea not found', mainTextArea); } debugLog('lastQueryBoxText', {lastQueryBoxText}); if (lastQueryBoxText) { const copilotNewThread = getCopilotNewThreadButton(); const copilotRepeatLast = getCopilotRepeatLastButton(); if (controlsArea.length > 0 && copilotNewThread.length < 1) { controlsArea.append(button('copilot_new_thread', robotIco, "Starts new thread for with last query text and Copilot ON", standardButtonCls)); } // Due to updates in Perplexity, this is unnecessary for now // if (controlsArea.length > 0 && copilotRepeatLast.length < 1) { // controlsArea.append(button('copilot_repeat_last', robotRepeatIco, "Repeats last query with Copilot ON")); // } if (!copilotNewThread.attr('data-has-custom-click-event')) { copilotNewThread.on("click", function () { debugLog('copilotNewThread Button clicked!'); openNewThreadModal(getLastQuery()); }) copilotNewThread.attr('data-has-custom-click-event', true); } if (!copilotRepeatLast.attr('data-has-custom-click-event')) { copilotRepeatLast.on("click", function () { const controlsArea = getCurrentControlsArea(); const textAreaElement = controlsArea.parent().find('textarea')[0]; const coPilotRepeatLastAutoSubmit = getSavedStates() ? getSavedStates().coPilotRepeatLastAutoSubmit : getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked'); debugLog('coPilotRepeatLastAutoSubmit', coPilotRepeatLastAutoSubmit); changeValueUsingEvent(textAreaElement, getLastQuery()); const copilotToggleButton = getCopilotToggleButton(mainTextArea) debugLog('mainTextArea', mainTextArea); debugLog('copilotToggleButton', copilotToggleButton); toggleBtnDot(copilotToggleButton, true); const isCopilotOnBtn = () => isCopilotOn(copilotToggleButton); const copilotCheck = () => { const ctx = {timer: null}; ctx.timer = setInterval(() => checkForCopilotToggleState(ctx.timer, isCopilotOnBtn, coPilotRepeatLastAutoSubmit, 0), 500); } copilotCheck(); debugLog('copilot_repeat_last Button clicked!'); }) copilotRepeatLast.attr('data-has-custom-click-event', true); } } if (getNumberOfDashedSVGs() > 0 && getNumberOfDashedSVGs() === getDashedCheckboxButton().length && getSelectAllButton().length < 1 && getSelectAllAndSubmitButton().length < 1) { debugLog('getNumberOfDashedSVGs() === getNumberOfDashedSVGs()', getNumberOfDashedSVGs()); debugLog('getSpecifyQuestionBox', getSpecifyQuestionBox()); const specifyQuestionControlsWrapper = getSpecifyQuestionControlsWrapper(); debugLog('specifyQuestionControlsWrapper', specifyQuestionControlsWrapper); const selectAllButton = textButton('perplexity_helper_select_all', 'Select all', 'Selects all options'); const selectAllAndSubmitButton = textButton('perplexity_helper_select_all_and_submit', 'Select all & submit', 'Selects all options and submits'); specifyQuestionControlsWrapper.append(selectAllButton); specifyQuestionControlsWrapper.append(selectAllAndSubmitButton); getSelectAllButton().on("click", function () { selectAllCheckboxes(); }) getSelectAllAndSubmitButton().on("click", function () { selectAllCheckboxes(); setTimeout(() => { getSpecifyQuestionControlsWrapper().find('button:contains("Continue")').click(); }, 200); }) } const constructClipBoard = (buttonId, buttonGetter, modalGetter, copiedModalId, elementGetter) => { const placeholderValue = getSpecifyQuestionBox().find('textarea').attr('placeholder') const clipboardInstance = new ClipboardJS(`#${buttonId}`, { text: () => placeholderValue }); const copiedModal = `<span id="${copiedModalId}">Copied!</span>`; debugLog('copiedModalId', copiedModalId); debugLog('copiedModal', copiedModal); jq('main').append(copiedModal); clipboardInstance.on('success', _ => { var buttonPosition = buttonGetter().position(); jq(`#${copiedModalId}`).css({ top: buttonPosition.top - 30, left: buttonPosition.left + 50 }).show(); if (elementGetter !== undefined) { changeValueUsingEvent(elementGetter()[0], placeholderValue); } setTimeout(() => { modalGetter().hide(); }, 5000); }); } if (questionBoxWithPlaceholderExists() && getCopyPlaceholder().length < 1) { const copyPlaceholder = textButton('perplexity_helper_copy_placeholder', 'Copy placeholder', 'Copies placeholder value'); const copyPlaceholderAndFillIn = textButton('perplexity_helper_copy_placeholder_and_fill_in', 'Copy placeholder and fill in', 'Copies placeholder value and fills in input'); const specifyQuestionControlsWrapper = getSpecifyQuestionControlsWrapper(); specifyQuestionControlsWrapper.append(copyPlaceholder); specifyQuestionControlsWrapper.append(copyPlaceholderAndFillIn); constructClipBoard('perplexity_helper_copy_placeholder', getCopyPlaceholder, getCopiedModal, 'copied-modal') constructClipBoard('perplexity_helper_copy_placeholder_and_fill_in', getCopyAndFillInPlaceholder, getCopiedModal2, 'copied-modal-2', getCopyPlaceholderInput) } } }; const fontUrls = { Roboto: 'https://fonts.cdnfonts.com/css/roboto', Montserrat: 'https://fonts.cdnfonts.com/css/montserrat', Lato: 'https://fonts.cdnfonts.com/css/lato', Oswald: 'https://fonts.cdnfonts.com/css/oswald-4', Raleway: 'https://fonts.cdnfonts.com/css/raleway-5', 'Ubuntu Mono': 'https://fonts.cdnfonts.com/css/ubuntu-mono', Nunito: 'https://fonts.cdnfonts.com/css/nunito', Poppins: 'https://fonts.cdnfonts.com/css/poppins', 'Playfair Display': 'https://fonts.cdnfonts.com/css/playfair-display', Merriweather: 'https://fonts.cdnfonts.com/css/merriweather', 'Fira Sans': 'https://fonts.cdnfonts.com/css/fira-sans', Quicksand: 'https://fonts.cdnfonts.com/css/quicksand', Comfortaa: 'https://fonts.cdnfonts.com/css/comfortaa-3', 'Almendra': 'https://fonts.cdnfonts.com/css/almendra', 'Enchanted Land': 'https://fonts.cdnfonts.com/css/enchanted-land', 'Cinzel Decorative': 'https://fonts.cdnfonts.com/css/cinzel-decorative', 'Orbitron': 'https://fonts.cdnfonts.com/css/orbitron', 'Exo 2': 'https://fonts.cdnfonts.com/css/exo-2', 'Chakra Petch': 'https://fonts.cdnfonts.com/css/chakra-petch', 'Open Sans Condensed': 'https://fonts.cdnfonts.com/css/open-sans-condensed', 'Saira Condensed': 'https://fonts.cdnfonts.com/css/saira-condensed', }; const loadFont = (fontName) => { const fontUrl = fontUrls[fontName]; debugLog('loadFont', { fontName, fontUrl }); if (fontUrl) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = fontUrl; document.head.appendChild(link); } }; (function () { if (loadConfigOrDefault()?.enableDebug) { enableDebugMode(); } debugLog('TAGS_PALETTES', TAGS_PALETTES); if (loadConfigOrDefault()?.debugTagsMode) { enableTagsDebugging(); } 'use strict'; jq("head").append(`<style>${styles}</style>`); setupTags(); const mainInterval = setInterval(work, 1000); window.ph = { stopWork: () => { clearInterval(mainInterval); }, work, jq, showPerplexityHelperModal, enableTagsDebugging: () => { debugTags = true; }, disableTagsDebugging: () => { debugTags = false; }, } loadFont(loadConfigOrDefault().tagFont); }());