您需要先安装一个扩展,例如 篡改猴、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 5.5 // @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 // @require https://cdnjs.cloudflare.com/ajax/libs/jsondiffpatch/0.4.1/jsondiffpatch.umd.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/lib/perplex-plus.js // @homepageURL https://www.perplexity.ai/ // @license GPL-3.0-or-later // ==/UserScript== const PP = window.PP.noConflict(); const jq = PP.jq; 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 dropRightStr = n => str => str.slice(0, -n); const filter = pred => xs => xs.filter(pred); 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); } }; 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); // 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 getTDesignIconUrl = iconName => `https://api.iconify.design/tdesign:${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) => `<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 modalSettingsTitleCls = genCssName('modal-settings-title'); const gitlabLogo = classes => ` <svg class="${classes}" fill="#000000" width="800px" height="800px" viewBox="0 0 512 512" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><path d="M494.07,281.6l-25.18-78.08a11,11,0,0,0-.61-2.1L417.78,44.48a20.08,20.08,0,0,0-19.17-13.82A19.77,19.77,0,0,0,379.66,44.6L331.52,194.15h-152L131.34,44.59a19.76,19.76,0,0,0-18.86-13.94h-.11a20.15,20.15,0,0,0-19.12,14L42.7,201.73c0,.14-.11.26-.16.4L16.91,281.61a29.15,29.15,0,0,0,10.44,32.46L248.79,476.48a11.25,11.25,0,0,0,13.38-.07L483.65,314.07a29.13,29.13,0,0,0,10.42-32.47m-331-64.51L224.8,408.85,76.63,217.09m209.64,191.8,59.19-183.84,2.55-8h86.52L300.47,390.44M398.8,59.31l43.37,134.83H355.35M324.16,217l-43,133.58L255.5,430.14,186.94,217M112.27,59.31l43.46,134.83H69M40.68,295.58a6.19,6.19,0,0,1-2.21-6.9l19-59L197.08,410.27M470.34,295.58,313.92,410.22l.52-.69L453.5,229.64l19,59a6.2,6.2,0,0,1-2.19,6.92"/></svg> `; const modalLargeIconAnchorClasses = 'hover:scale-110 opacity-50 hover:opacity-100 transition-all duration-300'; const modalTabGroupTabsCls = genCssName('modal-tab-group-tabs'); const modalTabGroupActiveCls = genCssName('modal-tab-group-active'); const modalTabGroupContentCls = genCssName('modal-tab-group-content'); const modalTabGroupSeparatorCls = genCssName('modal-tab-group-separator'); const modalHTML = ` <div id="${perplexityHelperModalId}" class="modal"> <div class="modal-content"> <span class="close">×</span> <h1 class="flex items-center gap-4"> <span class="mr-4 ${modalSettingsTitleCls}">Perplexity Helper</span> <a href="https://gitlab.com/Tiartyos/perplexity-helper" target="_blank" title="GitLab Repository" class="${modalLargeIconAnchorClasses}" > ${gitlabLogo('w-8 h-8 invert')} </a> <a href="https://tiartyos.gitlab.io/perplexity-helper/" target="_blank" title="Web Page" class="${modalLargeIconAnchorClasses}" > <img src="${getLucideIconUrl('globe')}" class="w-8 h-8 invert"> </a> </h1> <p class="text-xs opacity-30 mt-1 mb-3">Changes may require page refresh.</p> <div class="${modalTabGroupTabsCls}"> </div> <hr class="!mt-0 !mb-0 ${modalTabGroupSeparatorCls}"> </div> </div> `; const tagsContainerCls = genCssName('tags-container'); const tagContainerCompactCls = genCssName('tag-container-compact'); const tagContainerWiderCls = genCssName('tag-container-wider'); const tagContainerWideCls = genCssName('tag-container-wide'); const tagContainerExtraWideCls = genCssName('tag-container-extra-wide'); 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 tagsPreviewNewInCollectionCls = genCssName('tags-preview-new-in-collection'); const tagTweakTextShadowCls = genCssName('tag-tweak-text-shadow'); const tagFenceCls = genCssName('tag-fence'); const tagAllFencesWrapperCls = genCssName('tag-all-fences-wrapper'); const tagRestOfTagsWrapperCls = genCssName('tag-rest-of-tags-wrapper'); const tagFenceContentCls = genCssName('tag-fence-content'); const tagDirectoryCls = genCssName('tag-directory'); const tagDirectoryContentCls = genCssName('tag-directory-content'); 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 lucideIconParentCls = genCssName('lucide-icon-parent'); const roundedMD = genCssName('rounded-md'); const leftPanelSlimCls = genCssName('left-panel-slim'); const modelIconButtonCls = genCssName('model-icon-button'); const modelLabelCls = genCssName('model-label'); const modelLabelStyleJustTextCls = genCssName('model-label-style-just-text'); const modelLabelStyleButtonSubtleCls = genCssName('model-label-style-button-subtle'); const modelLabelStyleButtonWhiteCls = genCssName('model-label-style-button-white'); const modelLabelStyleButtonCyanCls = genCssName('model-label-style-button-cyan'); const modelLabelOverwriteCyanIconToGrayCls = genCssName('model-label-overwrite-cyan-icon-to-gray'); const modelLabelRemoveCpuIconCls = genCssName('model-label-remove-cpu-icon'); const reasoningModelCls = genCssName('reasoning-model'); const notReasoningModelCls = genCssName('not-reasoning-model'); const iconColorCyanCls = genCssName('icon-color-cyan'); const iconColorGrayCls = genCssName('icon-color-gray'); const iconColorWhiteCls = genCssName('icon-color-white'); const iconColorGoldCls = genCssName('icon-color-gold'); const customJsAppliedCls = genCssName('customJsApplied'); const customCssAppliedCls = genCssName('customCssApplied'); const customWidgetsHtmlAppliedCls = genCssName('customWidgetsHtmlApplied'); const sideMenuHiddenCls = genCssName('side-menu-hidden'); const sideMenuLabelsHiddenCls = genCssName('side-menu-labels-hidden'); const topSettingsButtonId = genCssName('settings-button-top'); const leftSettingsButtonId = genCssName('settings-button-left'); const leftSettingsButtonWrapperId = genCssName('settings-button-left-wrapper'); const cyanPerplexityColor = '#1fb8cd'; const cyanMediumPerplexityColor = '#204b51'; const cyanDarkPerplexityColor = '#203133'; const styles = ` .textarea_wrapper { display: flex; flex-direction: column; } @import url('https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600&display=swap'); .textarea_wrapper > textarea { width: 100%; background-color: rgba(0, 0, 0, 0.8); padding: 0 5px; border-radius: 0.5em; } .textarea_label { } .${helpTextCls} { background-color: #225; padding: 0.3em 0.7em; border-radius: 0.5em; margin: 1em 0; } .${helpTextCls} { cursor: text; } .${helpTextCls} a { text-decoration: underline; } .${helpTextCls} a:hover { color: white; } .${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); } .${helpTextCls} table { border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 0.5em; display: inline-block; } .${helpTextCls} table td, .${helpTextCls} table th { padding: 0.1em 0.5em; } .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 #333; background: linear-gradient(135deg, #151517, #202025); border-radius: 6px; color: rgb(206, 206, 210); flex-direction: column; position: relative; overflow-y: auto; cursor: default; font-family: 'Fira Sans', sans-serif; } .${modalTabGroupTabsCls} { display: flex; flex-direction: row; } .modal-content .${modalTabGroupTabsCls} > button { border-radius: 0.5em 0.5em 0 0; border-bottom: 0; padding: 0.2em 0.5em 0 0.5em; background-color: #1e293b; color: rgba(255, 255, 255, 0.5); outline-bottom: none; white-space: nowrap; } .modal-content .${modalTabGroupTabsCls} > button.${modalTabGroupActiveCls} { /* background-color: #3b82f6; */ color: white; text-shadow: 0 0 1px currentColor; padding: 0.3em 0.5em 0.2em 0.5em; } .modal-content .${modalTabGroupContentCls} { display: flex; flex-direction: column; gap: 1em; padding-top: 1em; } .${modalSettingsTitleCls} { background: linear-gradient(to bottom, white, gray); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; font-weight: bold; font-size: 3em; text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); user-select: none; margin-top: -0.33em; margin-bottom: -0.33em; } .${modalSettingsTitleCls} .animate-letter { display: inline-block; background: inherit; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; transition: transform 0.3s ease-out; } .${modalSettingsTitleCls} .animate-letter.active { /* Move and highlight on active */ transform: translateY(-10px) rotate(5deg); -webkit-text-fill-color: #4dabff; text-shadow: 0 0 5px #4dabff, 0 0 10px #4dabff; } .modal-content .hover\\:scale-110:hover { transform: scale(1.1); } .modal-content label { padding-right: 10px; } .modal-content hr { height: 1px; margin: 1em 0; border-color: rgba(255, 255, 255, 0.1); } .modal-content hr.${modalTabGroupSeparatorCls} { margin: 0 -1em 0 -1em; } .modal-content input[type="checkbox"] { appearance: none; width: 1.2em; height: 1.2em; border: 2px solid #ffffff80; border-radius: 0.25em; background-color: transparent; transition: all 0.2s ease; cursor: pointer; position: relative; } .modal-content input[type="checkbox"]:checked { background-color: #3b82f6; border-color: #3b82f6; } .modal-content input[type="checkbox"]:checked::after { content: ''; position: absolute; left: 50%; top: 50%; width: 0.4em; height: 0.7em; border: solid white; border-width: 0 2px 2px 0; transform: translate(-50%, -60%) rotate(45deg); } .modal-content input[type="checkbox"]:hover { border-color: #ffffff; } .modal-content input[type="checkbox"]:focus { outline: 2px solid #3b82f680; outline-offset: 2px; } .modal-content .checkbox_label { color: white; line-height: 1.5; } .modal-content .checkbox_wrapper { display: flex; align-items: center; gap: 0.5em; } .modal-content .number_label { margin-left: 0.5em; } .modal-content .color_wrapper { display: flex; align-items: center; } .modal-content .color_label { margin-left: 0.5em; } .modal-content input, .modal-content button { background-color: #1e293b; border: 2px solid #ffffff80; border-radius: 0.5em; color: white; padding: 0.5em; transition: border-color 0.3s ease, outline 0.3s ease; } .modal-content input:hover, .modal-content button:hover { border-color: #ffffff; } .modal-content input:focus, .modal-content button:focus { outline: 2px solid #3b82f680; outline-offset: 2px; } .modal-content input[type="number"] { padding: 0.5em; transition: border-color 0.3s ease, outline 0.3s ease; } .modal-content input[type="color"] { padding: 0; height: 2em; } .modal-content input[type="color"]:hover { border-color: #ffffff; } .modal-content input[type="color"]:focus { outline: 2px solid #3b82f680; outline-offset: 2px; } .modal-content h1 + hr { margin-top: 0.5em; } .modal-content select { appearance: none; background-color: #1e293b; /* Dark blue background */ border: 2px solid #ffffff80; border-radius: 0.5em; padding: 0.3em 2em 0.3em 0.5em; color: white; font-size: 1em; cursor: pointer; transition: all 0.2s ease; background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3e%3cpath d='M7 10l5 5 5-5z'/%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right 0.5em center; background-size: 1.2em; } .modal-content select option { background-color: #1e293b; /* Match select background */ color: white; padding: 0.5em; } .modal-content select:hover { border-color: #ffffff; } .modal-content select:focus { outline: 2px solid #3b82f680; outline-offset: 2px; } .modal-content .select_label { color: white; margin-left: 0.5em; } .modal-content .select_wrapper { display: flex; align-items: center; } .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; flex-direction: row; margin: 5px 0; } .${tagsContainerCls}.${threadTagContainerCls} { margin-left: 0.5em; margin-right: 0.5em; margin-bottom: 2px; } .${tagContainerCompactCls} { margin-top: -2em; margin-bottom: 1px; } .${tagContainerCompactCls} .${tagFenceCls} { margin: 0; padding: 1px; } .${tagContainerCompactCls} .${tagCls} { } .${tagContainerCompactCls} .${tagAllFencesWrapperCls} { gap: 1px; } .${tagContainerCompactCls} .${tagRestOfTagsWrapperCls} { margin: 1px; } .${tagContainerCompactCls} .${tagRestOfTagsWrapperCls}, .${tagContainerCompactCls} .${tagFenceContentCls}, .${tagContainerCompactCls} .${tagDirectoryContentCls} { gap: 1px; } .${tagContainerWiderCls} { margin-left: -6em; margin-right: -6em; } .${tagContainerWiderCls} .${tagCls} { } .${tagContainerWideCls} { margin-left: -12em; margin-right: -12em; } .${tagContainerExtraWideCls} { margin-left: -16em; margin-right: -16em; max-width: 100vw; } .${tagsContainerCls} { @media (max-width: 768px) { margin-left: 0 !important; margin-right: 0 !important; } } .${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: #E8E8E6; user-select: none; } .${tagCls}.${tagDarkTextCls} { color: #171719; } .${tagCls} span { display: inline-block; } .${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; transform: scale(1.02); } .${tagCls}.${tagDarkTextCls}:hover { /* color: #171717; */ color: #2f2f2f; } .${tagCls}:active { transform: scale(0.98); } .${tagPaletteCls} { display: flex; flex-wrap: wrap; gap: 1px; } .${tagPaletteCls} .${tagPaletteItemCls} { text-shadow: 1px 0 1px black, -1px 0 1px black, 0 1px 1px black, 0 -1px 1px black; width: 40px; height: 25px; display: inline-block; text-align: center; padding: 0 2px; transition: color 0.2s, border 0.1s; border: 2px solid transparent; } .${tagPaletteItemCls}:hover { cursor: pointer; color: white; border: 2px solid white; } .${tagsPreviewCls} { background-color: #191a1a; padding: 0.5em 1em; border-radius: 1em; } .${tagAllFencesWrapperCls} { display: flex; flex-direction: row; gap: 5px; } .${tagRestOfTagsWrapperCls} { display: flex; flex-direction: row; flex-wrap: wrap; align-content: flex-start; gap: 5px; margin: 8px; } .${tagFenceCls} { display: flex; margin: 5px 0; padding: 5px; border-radius: 4px; } .${tagFenceContentCls} { display: flex; flex-direction: column; flex-wrap: wrap; gap: 5px; } .${tagDirectoryCls} { position: relative; display: flex; z-index: 100; } .${tagDirectoryCls}:hover .${tagDirectoryContentCls} { display: flex; } .${tagDirectoryContentCls} { position: absolute; display: none; flex-direction: column; gap: 5px; top: 0px; padding-bottom: 1px; left: -5px; transform: translateY(-100%); background: rgba(0, 0, 0, 0.5); padding: 5px; border-radius: 4px; flex-wrap: nowrap; width: max-content; } .${tagDirectoryContentCls} .${tagCls} { white-space: nowrap; width: fit-content; } .${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.2em; justify-content: flex-start; } .w-collapsedSideBarWidth #${leftSettingsButtonWrapperId} { justify-content: center; } .${lucideIconParentCls} > img { transition: opacity 0.2s ease; } .${lucideIconParentCls}:hover > img, a.dark\\:text-textMainDark .${lucideIconParentCls} > img { opacity: 1; } .${leftPanelSlimCls} > .py-md { margin-left: -0.1em; } .${leftPanelSlimCls} > .py-md > div.flex-col > * { /* background: red; */ margin-right: 0; max-width: 40px; } .${modelLabelCls} { color: #888; /* padding is from style attr */ transition: color 0.2s, background-color 0.2s, border 0.2s; /* margin-right: 0.5em; margin-left: 0.5em; margin-right: 0.5em; */ padding-top: 2px; } button.${modelIconButtonCls} { padding-right: 1.0em; padding-left: 1.0em; gap: 5px; } button:hover > .${modelLabelCls} { color: #fff; } button.${modelIconButtonCls} > .min-w-0 { min-width: 16px; margin-right: 0.0em; } button.${modelLabelRemoveCpuIconCls} { margin-left: 0.5em; padding-right: 1.0em; } button.${modelLabelRemoveCpuIconCls} .${modelLabelCls} { margin-right: 0.5em; } button.${modelLabelRemoveCpuIconCls}:has(.${reasoningModelCls}) .${modelLabelCls} { margin-right: 0.5em; } button.${modelLabelRemoveCpuIconCls}.${notReasoningModelCls} .${modelLabelCls} { margin-right: 0.0em; } .${modelLabelRemoveCpuIconCls} div:has(div > svg.tabler-icon-cpu) { display: none; } button:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}) { border: 1px solid #333; } button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}) { background: #333 !important; } .${modelLabelCls}.${modelLabelStyleButtonWhiteCls} { color: #8D9191 !important; } button:hover > .${modelLabelCls}.${modelLabelStyleButtonWhiteCls} { color: #fff !important; } .${modelIconButtonCls} svg[stroke] { stroke: #8D9191 !important; } .${modelIconButtonCls}:hover svg[stroke] { stroke: #fff !important; } button:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}) { background: #191A1A !important; color: #2D2F2F !important; } button:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}):hover { color: #8D9191 !important; } .${modelLabelCls}.${modelLabelStyleButtonCyanCls} { color: ${cyanPerplexityColor}; } button:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) { border: 1px solid ${cyanMediumPerplexityColor}; background: ${cyanDarkPerplexityColor} !important; } button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) { border: 1px solid ${cyanPerplexityColor}; } .${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) svg[stroke] { stroke: ${cyanPerplexityColor} !important; } .${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}):hover svg[stroke] { stroke: #fff !important; } button:has(> .${modelLabelCls}.${modelLabelOverwriteCyanIconToGrayCls}) { color: #888 !important; } button:has(> .${modelLabelCls}.${modelLabelOverwriteCyanIconToGrayCls}):hover { color: #fff !important; } .${reasoningModelCls} { width: 16px; height: 16px; margin-right: 0px; margin-left: 0; margin-top: -2px; filter: invert(); } /* button:has(.${reasoningModelCls}) > div > div > svg { width: 32px; height: 16px; margin-left: 8px; margin-right: 12px; margin-top: 0px; min-width: 16px; background-color: cyan; } button:has(.${reasoningModelCls}) > div > div:has(svg) { width: 16px; height: 16px; min-width: 30px; background-color: purple; } */ .${iconColorCyanCls} { filter: invert(54%) sepia(84%) saturate(431%) hue-rotate(139deg) brightness(97%) contrast(90%); transition: filter 0.2s; } button:hover:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}) .${iconColorCyanCls} { filter: invert(100%); } .${iconColorGrayCls} { filter: invert(50%); transition: filter 0.2s; } button:has(.${reasoningModelCls}):hover .${iconColorGrayCls} { filter: invert(100%); } .${iconColorWhiteCls} { filter: invert(50%); transition: filter 0.2s; } button:has(.${reasoningModelCls}):hover .${iconColorWhiteCls} { filter: invert(100%); } .${iconColorGoldCls} { filter: brightness(0) saturate(100%) invert(88%) sepia(75%) saturate(2577%) hue-rotate(324deg) brightness(96%) contrast(99%); transition: filter 0.2s; } .${sideMenuHiddenCls} { display: none; } .${sideMenuLabelsHiddenCls} div.text-xs { display: none; } `; const TAG_POSITION = { BEFORE: 'before', AFTER: 'after', CARET: 'caret', WRAP: 'wrap', }; 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) or \`wrap\` (wrap text around \`$$wrap$$\` marker) - \`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). prefix \`td:\` is used for [TDesign icons](https://tdesign.tencent.com/design/icon-en#header-69). prefix \`l:\` for Lucide icons is implicit and can be omitted. - \`set-mode\`: set the query mode: \`pro\` or \`research\`, e.g. \`<set-mode:pro>\` - \`set-model\`: set the model, e.g. \`<set-model:claude-3-7-sonnet-thinking>\` - \`set-sources\`: set the sources, e.g. \`<set-sources:001>\` for disabled first source (web), disabled second source (academic), enabled third source (social) - \`auto-submit\`: automatically submit the query after the tag is clicked (applies after other tag actions like \`set-mode\` or \`set-model\`), e.g. \`<auto-submit>\` - \`dir\`: unique identifier for a directory tag (it will not insert text into prompt) - \`in-dir\`: identifier of the parent directory this tag belongs to - \`fence\`: unique identifier for a fence definition (hidden by default) - \`in-fence\`: identifier of the fence this tag belongs to - \`fence-width\`: CSS width for a fence, e.g. \`<fence-width:10em>\` - \`fence-border-style\`: CSS border style for a fence (e.g., solid, dashed, dotted) - \`fence-border-color\`: CSS color or a palette \`%\` syntax for a fence border - \`fence-border-width\`: CSS width for a fence border --- | String | Replacement | Example | |---|---|---| | \`\\n\` | newline | | | \`$$time$$\` | current time | \`23:05\` | | \`$$wrap$$\` | sets position where existing text will be inserted | | --- Examples: \`\`\` stable diffusion web ui - <label:SDWU> , prefer concise modern syntax and style, <position:caret><label:concise modern> tell me a joke<label:Joke><tooltip:> tell me a joke<label:Joke & Submit><auto-submit> \`\`\` Directory example: \`\`\` <dir:games>Games<icon:gamepad-2> <in-dir:games>FFXIV: <color:%15><label:FFXIV> <in-dir:games>Vintage Story - <label:VS> \`\`\` Fence example: \`\`\` <fence:anime><fence-border-style:dashed><fence-border-color:%10> <in-fence:anime>Shounen <in-fence:anime>Seinen <in-fence:anime>Shoujo \`\`\` Another fence example: \`\`\` <fence:programming><fence-border-style:solid><fence-border-color:%20> <in-fence:programming>Haskell <in-fence:programming>Raku<label:🦋> \`\`\` `.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 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(cyanPerplexityColor); 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(cyanPerplexityColor); return _.flow( _.map(x => startH + x * step, _), _.map(h => color2k.hsla(h, startS - 0.2, 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(cyanPerplexityColor); 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(cyanPerplexityColor); 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(cyanPerplexityColor); 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, CUSTOM: 'CUSTOM', }); const convertColorInPaletteFormat = currentPalette => value => currentPalette[parseInt(dropStr(1)(value), 10)] ?? defaultTagColor; const TAG_HOME_PAGE_LAYOUT = { DEFAULT: 'default', COMPACT: 'compact', WIDER: 'wider', WIDE: 'wide', EXTRA_WIDE: 'extra-wide', }; const parseBinaryState = binaryStr => { if (!/^[01-]+$/.test(binaryStr)) { throw new Error('Invalid binary state: ' + binaryStr); } return binaryStr.split('').map(bit => bit === '1' ? true : bit === '0' ? false : null); }; const processTagField = currentPalette => name => value => { if (name === 'color' && value.startsWith('%')) return convertColorInPaletteFormat(currentPalette)(value); if (name === 'hide') return true; if (name === 'auto-submit') return true; if (name === 'set-sources') return parseBinaryState(value); return value; }; const tagLineRegex = /<(label|position|color|tooltip|target|hide|link|link-target|icon|dir|in-dir|fence|in-fence|fence-border-style|fence-border-color|fence-border-width|fence-width|set-mode|set-model|auto-submit|set-sources)(?::([^<>]*))?>/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, [_.camelCase(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)).map((x, i) => ({ ...x, originalIndex: i })); }; const getTagsContainer = () => $c(tagsContainerCls); const posFromTag = tag => Object.values(TAG_POSITION).includes(tag.position) ? tag.position : TAG_POSITION.BEFORE; const splitTextAroundWrap = (text) => { const parts = text.split('$$wrap$$'); return { before: parts[0] || '', after: parts[1] || '', }; }; const applyTagToString = (tag, val, caretPos) => { const { text } = tag; const timeString = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); const textAfterTime = text.replace(/\$\$time\$\$/g, timeString); const { before: processedTextBefore, after: processedTextAfter } = splitTextAroundWrap(textAfterTime); const processedText = processedTextBefore; switch (posFromTag(tag)) { case TAG_POSITION.BEFORE: return `${processedText}${val}`; case TAG_POSITION.AFTER: return `${val}${processedText}`; case TAG_POSITION.CARET: return `${takeStr(caretPos)(val)}${processedText}${dropStr(caretPos)(val)}`; case TAG_POSITION.WRAP: return `${processedTextBefore}${val}${processedTextAfter}`; default: throw new Error(`Invalid position: ${tag.position}`); } }; const getPromptAreaFromTagsContainer = tagsContainerEl => PP.getAnyPromptArea(tagsContainerEl.parent()); const getPalette = paletteName => { // Add this check for 'CUSTOM' if (paletteName === TAGS_PALETTES.CUSTOM) { // Use tagPaletteCustom from config or default if not found return loadConfigOrDefault()?.tagPaletteCustom ?? defaultConfig.tagPaletteCustom; } // Fallback to predefined palettes or CLASSIC as default const palette = TAGS_PALETTES[paletteName]; // Check if palette is an array before returning, otherwise return default return Array.isArray(palette) ? palette : 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) > loadConfig().tagLuminanceThreshold; 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; if (tag.setMode) { switch (tag.setMode) { case 'pro': PP.getModeProButton().click(); break; case 'research': case 'deep-research': case 'dr': PP.getModeResearchButton().click(); break; default: throw new Error(`Invalid set-mode: ${tag.setMode}`); } } if (tag.setModel) { setTimeout(() => { // delay for model button to be available after setting mode const modelDescriptor = PP.getModelDescriptorFromId(tag.setModel); debugLog('[createTag] clickHandler: set model=', tag.setModel, ' modelDescriptor=', modelDescriptor); PP.doSelectModel(modelDescriptor.index); }, 50); } if (tag.setSources) { setTimeout(() => { PP.getAnySourcesButton().click(); setTimeout(() => { PP.setSourcesSelectionListValues()(tag.setSources, { callback: () => { debugLogTags('[createTag] clickHandler: setSources callback'); setTimeout(() => { PP.getAnySourcesButton().click(); }, 5); }, }); debugLogTags('[createTag] clickHandler: setSources=', tag.setSources); }, 10); }, 80); } if (tag.autoSubmit) { setTimeout(() => { const submitButton = PP.submitButtonAny(); debugLogTags('[createTag] clickHandler: submitButton=', submitButton); if (submitButton.length) { if (submitButton.length > 1) { debugLogTags('[createTag] clickHandler: multiple submit buttons found, using first one'); } submitButton.first().click(); } else { debugLogTags('[createTag] clickHandler: no submit button found'); } }, 300); } const el = jq(evt.currentTarget); const tagsContainer = el.closest(`.${tagsContainerCls}`); if (!tagsContainer.length) { debugLogTags('[clickHandler] no tags container found'); return; } const promptArea = getPromptAreaFromTagsContainer(tagsContainer); if (!promptArea.length) { debugLogTags('[clickHandler] 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, borderRadius: `${loadConfig().tagRoundness}px`, }) .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) .css({ 'font-weight': loadConfig().tagBold ? 'bold' : 'normal', 'font-style': loadConfig().tagItalic ? 'italic' : 'normal', 'font-size': `${loadConfig().tagFontSize}px`, 'transform': `translateY(${loadConfig().tagTextYOffset}px)`, }); if (tag.icon) { const iconEl = jq('<img/>') .attr('src', getIconUrl(tag.icon)) .addClass(tagIconCls) .css({ 'width': `${loadConfig().tagIconSize}px`, 'height': `${loadConfig().tagIconSize}px`, 'transform': `translateY(${loadConfig().tagIconYOffset}px)`, }); if (!labelString) { iconEl.css({ marginLeft: '0', marginRight: '0', }); } textEl.prepend(iconEl); } tagEl.append(textEl); if (tag.link) { const linkEl = jq('<a/>') .attr('href', tag.link) .attr('target', tag.linkTarget) .css({ textDecoration: 'none', color: 'inherit' }); textEl.wrap(linkEl); } if (!isPreview && !tag.link && !tag.dir) { 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 getPromptWrapperTagContainerType = promptWrapper => { if (PP.getPromptAreaOfNewThread(promptWrapper).length) return TAG_CONTAINER_TYPE.NEW; if (PP.getPromptAreaOnThread(promptWrapper).length) return TAG_CONTAINER_TYPE.THREAD; if (PP.getPromptAreaOnCollection(promptWrapper).length) 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 tagContainerTypeToTagContainerClass = { [TAG_CONTAINER_TYPE.THREAD]: threadTagContainerCls, [TAG_CONTAINER_TYPE.NEW]: newTagContainerCls, [TAG_CONTAINER_TYPE.NEW_IN_COLLECTION]: newTagContainerInCollectionCls, }; const currentUrlIsSettingsPage = () => window.location.pathname.includes('/settings/'); const refreshTags = ({ force = false } = {}) => { if (!loadConfigOrDefault()?.tagsEnabled) return; const promptWrapper = PP.getPromptAreaWrapperOfNewThread() .add(PP.getPromptAreaWrapperOnThread()) .add(PP.getPromptAreaWrapperOnCollection().nthParent(2)) .filter((_, rEl) => { const isPreview = Boolean(jq(rEl).attr('data-preview')); return isPreview || !currentUrlIsSettingsPage(); }); if (!promptWrapper.length) { debugLogTags('no prompt area found'); } // debugLogTags('promptWrappers', promptWrapper); 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); const tagContainerType = getPromptWrapperTagContainerType(promptWrapper); if (tagContainerType) { const clsToAdd = tagContainerTypeToTagContainerClass[tagContainerType]; if (!clsToAdd) { console.error('Unexpected tagContainerType:', tagContainerType, { promptWrapper }); } el.addClass(clsToAdd); } 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 currentPalette = getPalette(loadConfig().tagPalette); const createFence = (fence) => { const fenceEl = jq('<div/>') .addClass(tagFenceCls) .css({ 'border-style': fence.fenceBorderStyle ?? 'solid', 'border-color': fence.fenceBorderColor?.startsWith('%') ? convertColorInPaletteFormat(currentPalette)(fence.fenceBorderColor) : fence.fenceBorderColor ?? defaultTagColor, 'border-width': fence.fenceBorderWidth ?? '1px', }) .attr('data-tag', JSON.stringify(fence)) ; const fenceContentEl = jq('<div/>') .addClass(tagFenceContentCls) .css({ 'width': fence.fenceWidth ?? '', }) ; fenceEl.append(fenceContentEl); return { fenceEl, fenceContentEl }; }; const createDirectory = () => { const directoryEl = jq('<div/>').addClass(tagDirectoryCls); const directoryContentEl = jq('<div/>').addClass(tagDirectoryContentCls); directoryEl.append(directoryContentEl); return { directoryEl, directoryContentEl }; }; const containerEls = getTagsContainer(); containerEls.each((_i, rEl) => { const containerEl = jq(rEl); const isPreview = Boolean(containerEl.attr('data-preview')); const tagContainerTypeFromPromptWrapper = getPromptWrapperTagContainerType(containerEl.nthParent(2)); const prelimTagContainerType = getTagContainerType(containerEl); if (tagContainerTypeFromPromptWrapper !== prelimTagContainerType && !isPreview) { debugLog('tagContainerTypeFromPromptWrapper !== prelimTagContainerType', { tagContainerTypeFromPromptWrapper, prelimTagContainerType, containerEl, isPreview }); containerEl .empty() .removeClass(threadTagContainerCls, newTagContainerCls, newTagContainerInCollectionCls) .addClass(tagContainerTypeToTagContainerClass[tagContainerTypeFromPromptWrapper]) ; } else { if (!isPreview) { debugLogTags('tagContainerTypeFromPromptWrapper === prelimTagContainerType', { tagContainerTypeFromPromptWrapper, prelimTagContainerType, containerEl, isPreview }); } } // TODO: use something else than lodash/fp. in following functions it behaved randomly very weirdly // e.g. partial application of map resulting in an empty array or sortBy sorting field name instead // of input array. possibly inconsistent normal FP order of arguments const mapParseAttrTag = xs => xs.map(el => JSON.parse(el.dataset.tag)); const sortByOriginalIndex = xs => [...xs].sort((a, b) => a.originalIndex - b.originalIndex); const tagElsInCurrentContainer = containerEl.find(`.${tagCls}, .${tagFenceCls}`).toArray(); const filterOutHidden = filter(x => !x.hide); const currentTags = _.flow( mapParseAttrTag, sortByOriginalIndex, filterOutHidden, _.uniq, )(tagElsInCurrentContainer); const tagContainerType = getTagContainerType(containerEl); const tagsForThisContainer = _.flow( filter(isTagRelevantForContainer(tagContainerType)), filterOutHidden, sortByOriginalIndex, )(allTags); debugLogTags('tagContainerType =', tagContainerType, ', current tags =', currentTags, ', tagsForThisContainer =', tagsForThisContainer, ', tagElsInCurrentContainer =', tagElsInCurrentContainer); if (_.isEqual(currentTags, tagsForThisContainer) && !force) { debugLogTags('no tags changed'); return; } const diff = jsondiffpatch.diff(currentTags, tagsForThisContainer); const changedTags = jsondiffpatch.formatters.console.format(diff); debugLogTags('changedTags', changedTags); containerEl.empty(); const tagHomePageLayout = loadConfig()?.tagHomePageLayout; 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) { containerEl.addClass(tagContainerCompactCls); } } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.COMPACT) { containerEl.addClass(tagContainerCompactCls); } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.WIDER) { containerEl.addClass(tagContainerWiderCls); } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.WIDE) { containerEl.addClass(tagContainerWideCls); } else if (tagHomePageLayout === TAG_HOME_PAGE_LAYOUT.EXTRA_WIDE) { containerEl.addClass(tagContainerExtraWideCls); } else { containerEl.removeClass(`${tagContainerCompactCls} ${tagContainerWiderCls} ${tagContainerWideCls} ${tagContainerExtraWideCls}`); } const extraMargin = loadConfig()?.tagContainerExtraBottomMargin || 0; containerEl.css('margin-bottom', `${extraMargin}em`); } } const fences = {}; const directories = {}; const fencesWrapperEl = jq('<div/>').addClass(tagAllFencesWrapperCls); const restWrapperEl = jq('<div/>').addClass(tagRestOfTagsWrapperCls); tagsForThisContainer.forEach(tag => { const { fence, dir, inFence, inDir } = tag; const getOrCreateDirectory = dirName => { if (!directories[dirName]) directories[dirName] = createDirectory(); return directories[dirName]; }; const getTagContainer = () => { if (fence) { if (!fences[fence]) fences[fence] = createFence(tag); return fences[fence].fenceContentEl; } else if (dir && inFence) { if (!fences[inFence]) { console.error(`fence ${inFence} for tag not found`, tag); return null; } const { directoryEl } = getOrCreateDirectory(dir); fences[inFence].fenceContentEl.append(directoryEl); return directoryEl; } else if (dir) { const { directoryEl } = getOrCreateDirectory(dir); restWrapperEl.append(directoryEl); return directoryEl; } else if (inFence) { if (!fences[inFence]) { console.error(`fence ${inFence} for tag not found`, tag); return null; } return fences[inFence].fenceContentEl; } else if (inDir) { if (!directories[inDir]) { console.error(`directory ${inDir} for tag not found`, tag); return null; } return directories[inDir].directoryContentEl; } else { return restWrapperEl; } }; const tagContainer = getTagContainer(); if (tagContainer && !fence) { createTag(tagContainer)(isPreview)(tag); } }); Object.values(fences).forEach(({ fenceEl }) => fencesWrapperEl.append(fenceEl)); containerEl.append(fencesWrapperEl).append(restWrapperEl); }); }; const setupTags = () => { debugLog('setting up tags'); setInterval(refreshTags, 500); }; const ICON_REPLACEMENT_MODE = Object.freeze({ OFF: 'Off', LUCIDE1: 'Lucide 1', LUCIDE2: 'Lucide 2', LUCIDE3: 'Lucide 3', TDESIGN1: 'TDesign 1', TDESIGN2: 'TDesign 2', TDESIGN3: 'TDesign 3', }); const leftPanelIconMappingsToLucide1 = Object.freeze({ 'search': 'search', 'discover': 'telescope', 'spaces': 'shapes', }); const leftPanelIconMappingsToLucide2 = Object.freeze({ 'search': 'house', 'discover': 'compass', 'spaces': 'square-stack', 'library': 'archive', }); const leftPanelIconMappingsToLucide3 = Object.freeze({ 'search': 'search', 'discover': 'telescope', 'spaces': 'bot', 'library': 'folder-open', }); const leftPanelIconMappingsToTDesign1 = Object.freeze({ 'search': 'search', 'discover': 'compass-filled', 'spaces': 'grid-view', 'library': 'book', }); const leftPanelIconMappingsToTDesign2 = Object.freeze({ 'search': 'search', 'discover': 'shutter-filled', 'spaces': 'palette-1', 'library': 'folder-open-1-filled', }); const leftPanelIconMappingsToTDesign3 = Object.freeze({ 'search': 'search', 'discover': 'banana-filled', 'spaces': 'chili-filled', 'library': 'barbecue-filled', }); const iconMappings = { LUCIDE1: leftPanelIconMappingsToLucide1, LUCIDE2: leftPanelIconMappingsToLucide2, LUCIDE3: leftPanelIconMappingsToLucide3, TDESIGN1: leftPanelIconMappingsToTDesign1, TDESIGN2: leftPanelIconMappingsToTDesign2, TDESIGN3: leftPanelIconMappingsToTDesign3, }; const MODEL_LABEL_TEXT_MODE = Object.freeze({ OFF: 'Off', FULL_NAME: 'Full Name', SHORT_NAME: 'Short Name', PP_MODEL_ID: 'PP Model ID', OWN_NAME_VERSION_SHORT: 'Own Name + Version Short', }); const MODEL_LABEL_STYLE = Object.freeze({ OFF: 'Off', JUST_TEXT: 'Just Text', BUTTON_SUBTLE: 'Button Subtle', BUTTON_WHITE: 'Button White', BUTTON_CYAN: 'Button Cyan', }); const CUSTOM_MODEL_POPOVER_MODE = Object.freeze({ OFF: 'Off', SIMPLE_LIST: 'Simple List', COMPACT_LIST: 'Compact List', SIMPLE_GRID: 'Simple 2x Grid', COMPACT_GRID: 'Compact 2x Grid', }); const MODEL_LABEL_ICON_REASONING_MODEL = Object.freeze({ OFF: 'Off', LIGHTBULB: 'Lightbulb', BRAIN: 'Brain', MICROCHIP: 'Microchip', }); const defaultConfig = Object.freeze({ // General hideSideMenu: false, slimLeftMenu: false, hideSideMenuLabels: false, hideHomeWidgets: false, hideDiscoverButton: false, fixImageGenerationOverlay: false, extraSpaceBellowLastAnswer: false, replaceIconsInMenu: ICON_REPLACEMENT_MODE.OFF, // Model modelLabelTextMode: MODEL_LABEL_TEXT_MODE.OFF, modelLabelStyle: MODEL_LABEL_STYLE.OFF, modelLabelOverwriteCyanIconToGray: false, modelLabelUseIconForReasoningModels: MODEL_LABEL_ICON_REASONING_MODEL.OFF, modelLabelReasoningModelIconGold: false, modelLabelRemoveCpuIcon: false, customModelPopover: CUSTOM_MODEL_POPOVER_MODE.SIMPLE_GRID, // Legacy showCopilot: true, showCopilotNewThread: true, showCopilotRepeatLast: true, showCopilotCopyPlaceholder: true, // Tags tagsEnabled: true, tagsText: '', tagPalette: 'CLASSIC', tagPaletteCustom: ['#000', '#fff', '#ff0', '#f00', '#0f0', '#00f', '#0ff', '#f0f'], tagFont: 'Roboto', tagHomePageLayout: TAG_HOME_PAGE_LAYOUT.DEFAULT, tagContainerExtraBottomMargin: 0, tagLuminanceThreshold: 0.35, tagBold: false, tagItalic: false, tagFontSize: 16, tagIconSize: 16, tagRoundness: 4, tagTextYOffset: 0, tagIconYOffset: 0, // Raw mainCaptionHtml: '', customJs: '', customCss: '', customWidgetsHtml: '', // Settings // Debug debugMode: false, debugTagsMode: false, }); // 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(checkbox).append(' ').append(label); debugLog('checkboxwithlabel', checkboxWithLabel); getSettingsLastTabGroupContent().append(checkboxWithLabel); checkbox.on('change', onChange); return checkbox; }; const createTextArea = (id, labelText, onChange, helpText, links) => { debugLog("createTextArea", id); const textarea = jq(`<textarea id=${id}></textarea>`); const bookIconHtml = `<img src="${getLucideIconUrl('book-text')}" class="w-4 h-4 invert inline-block"/>`; const labelTextHtml = `<span class="opacity-100">${labelText}</span>`; const label = jq(`<label class="textarea_label">${labelTextHtml}${helpText ? ' ' + bookIconHtml : ''}</label>`); const labelWithLinks = jq('<div/>').addClass('flex flex-row gap-2 mb-2').append(label); const textareaWrapper = jq('<div class="textarea_wrapper"></div>').append(labelWithLinks); if (links) { links.forEach(({ icon, label, url, tooltip }) => { const iconHtml = `<img src="${getIconUrl(icon)}" class="w-4 h-4 invert opacity-50 hover:opacity-100 transition-opacity duration-300 ease-in-out"/>`; const link = jq(`<a href="${url}" target="_blank" class="flex flex-row gap-2 items-center">${icon ? iconHtml : ''}${label ? ' ' + label : ''}</a>`); link.attr('title', tooltip); labelWithLinks.append(link); }); } if (helpText) { const help = jq(`<div/>`).addClass(helpTextCls).html(markdownConverter.makeHtml(helpText)).append(jq('<br/>')); help.find('a').each((_, a) => jq(a).attr('target', '_blank')); help.append(jq('<button/>').text('[Close help]').on('click', () => help.hide())); textareaWrapper.append(help); label .css({ cursor: 'pointer' }) .on('click', () => help.toggle()) .prop('title', 'Click to toggle help') ; help.hide(); } textareaWrapper.append(textarea); debugLog('textareaWithLabel', textareaWrapper); getSettingsLastTabGroupContent().append(textareaWrapper); 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(select).append(label); debugLog('selectWithLabel', selectWithLabel); getSettingsLastTabGroupContent().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(input).append(label); debugLog('inputWithLabel', inputWithLabel); getSettingsLastTabGroupContent().append(inputWithLabel); input.on('change', onChange); return input; }; const createNumberInput = (id, labelText, onChange, { step = 1, min = 0, max = 100 } = {}) => { debugLog("createNumberInput", id); const input = jq(`<input type="number" id=${id}>`) .prop('step', step) .prop('min', min) .prop('max', max) ; const label = jq(`<label class="number_label">${labelText}</label>`); const inputWithLabel = jq('<div class="number_wrapper"></div>').append(input).append(label); debugLog('inputWithLabel', inputWithLabel); getSettingsLastTabGroupContent().append(inputWithLabel); input.on('change', onChange); return input; }; const createTagsPreview = () => { const wrapper = jq('<div/>') .addClass(tagsPreviewCls) .append(jq('<div>').text('Preview').addClass('text-lg font-bold')) .append(jq('<div>').text('Target New:')) .append(jq('<div>').addClass(tagsPreviewNewCls).addClass(tagsContainerCls).attr('data-preview', 'true')) .append(jq('<div>').text('Target Thread:')) .append(jq('<div>').addClass(tagsPreviewThreadCls).addClass(tagsContainerCls).attr('data-preview', 'true')) ; getSettingsLastTabGroupContent().append(wrapper); }; const coPilotNewThreadAutoSubmitCheckboxId = 'coPilotNewThreadAutoSubmit'; const getCoPilotNewThreadAutoSubmitCheckbox = () => $i(coPilotNewThreadAutoSubmitCheckboxId); const coPilotRepeatLastAutoSubmitCheckboxId = 'coPilotRepeatLastAutoSubmit'; const getCoPilotRepeatLastAutoSubmitCheckbox = () => $i(coPilotRepeatLastAutoSubmitCheckboxId); const hideSideMenuCheckboxId = 'hideSideMenu'; const getHideSideMenuCheckbox = () => $i(hideSideMenuCheckboxId); const tagsEnabledId = genCssName('tagsEnabled'); const getTagsEnabledCheckbox = () => $i(tagsEnabledId); 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 tagContainerExtraBottomMarginInputId = genCssName('tagContainerExtraBottomMargin'); const getTagContainerExtraBottomMarginInput = () => $i(tagContainerExtraBottomMarginInputId); const tagLuminanceThresholdInputId = genCssName('tagLuminanceThreshold'); const getTagLuminanceThresholdInput = () => $i(tagLuminanceThresholdInputId); const tagBoldCheckboxId = genCssName('tagBold'); const getTagBoldCheckbox = () => $i(tagBoldCheckboxId); const tagItalicCheckboxId = genCssName('tagItalic'); const getTagItalicCheckbox = () => $i(tagItalicCheckboxId); const tagFontSizeInputId = genCssName('tagFontSize'); const getTagFontSizeInput = () => $i(tagFontSizeInputId); const tagIconSizeInputId = genCssName('tagIconSize'); const getTagIconSizeInput = () => $i(tagIconSizeInputId); const tagRoundnessInputId = genCssName('tagRoundness'); const getTagRoundnessInput = () => $i(tagRoundnessInputId); const tagTextYOffsetInputId = genCssName('tagTextYOffset'); const getTagTextYOffsetInput = () => $i(tagTextYOffsetInputId); const tagIconYOffsetInputId = genCssName('tagIconYOffset'); const getTagIconYOffsetInput = () => $i(tagIconYOffsetInputId); const tagPaletteCustomTextAreaId = genCssName('tagPaletteCustomTextArea'); const getTagPaletteCustomTextArea = () => $i(tagPaletteCustomTextAreaId); const replaceIconsInMenuId = genCssName('replaceIconsInMenu'); const getReplaceIconsInMenu = () => $i(replaceIconsInMenuId); const slimLeftMenuCheckboxId = genCssName('slimLeftMenu'); const getSlimLeftMenuCheckbox = () => $i(slimLeftMenuCheckboxId); const hideHomeWidgetsCheckboxId = genCssName('hideHomeWidgets'); const getHideHomeWidgetsCheckbox = () => $i(hideHomeWidgetsCheckboxId); const hideDiscoverButtonCheckboxId = genCssName('hideDiscoverButton'); const getHideDiscoverButtonCheckbox = () => $i(hideDiscoverButtonCheckboxId); const fixImageGenerationOverlayCheckboxId = genCssName('fixImageGenerationOverlay'); const getFixImageGenerationOverlayCheckbox = () => $i(fixImageGenerationOverlayCheckboxId); const extraSpaceBellowLastAnswerCheckboxId = genCssName('extraSpaceBellowLastAnswer'); const getExtraSpaceBellowLastAnswerCheckbox = () => $i(extraSpaceBellowLastAnswerCheckboxId); const modelLabelTextModeSelectId = genCssName('modelLabelTextModeSelect'); const getModelLabelTextModeSelect = () => $i(modelLabelTextModeSelectId); const modelLabelStyleSelectId = genCssName('modelLabelStyleSelect'); const getModelLabelStyleSelect = () => $i(modelLabelStyleSelectId); const modelLabelOverwriteCyanIconToGrayCheckboxId = genCssName('modelLabelOverwriteCyanIconToGray'); const getModelLabelOverwriteCyanIconToGrayCheckbox = () => $i(modelLabelOverwriteCyanIconToGrayCheckboxId); const modelLabelUseIconForReasoningModelsSelectId = genCssName('modelLabelUseIconForReasoningModelsSelect'); const getModelLabelUseIconForReasoningModelsSelect = () => $i(modelLabelUseIconForReasoningModelsSelectId); const modelLabelReasoningModelIconGoldCheckboxId = genCssName('modelLabelReasoningModelIconGold'); const getModelLabelReasoningModelIconGoldCheckbox = () => $i(modelLabelReasoningModelIconGoldCheckboxId); const modelLabelRemoveCpuIconCheckboxId = genCssName('modelLabelRemoveCpuIcon'); const getModelLabelRemoveCpuIconCheckbox = () => $i(modelLabelRemoveCpuIconCheckboxId); const customModelPopoverSelectId = genCssName('customModelPopoverSelect'); const getCustomModelPopoverSelect = () => $i(customModelPopoverSelectId); const mainCaptionHtmlTextAreaId = genCssName('mainCaptionHtmlTextArea'); const getMainCaptionHtmlTextArea = () => $i(mainCaptionHtmlTextAreaId); const customJsTextAreaId = genCssName('customJsTextArea'); const getCustomJsTextArea = () => $i(customJsTextAreaId); const customCssTextAreaId = genCssName('customCssTextArea'); const getCustomCssTextArea = () => $i(customCssTextAreaId); const customWidgetsHtmlTextAreaId = genCssName('customWidgetsHtmlTextArea'); const getCustomWidgetsHtmlTextArea = () => $i(customWidgetsHtmlTextAreaId); const hideSideMenuLabelsId = genCssName('hideSideMenuLabels'); const getHideSideMenuLabels = () => $i(hideSideMenuLabelsId); 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}>`; const loadCurrentConfigToSettingsForm = () => { const savedStatesRaw = JSON.parse(localStorage.getItem(storageKey)); if (savedStatesRaw === null) { return; } const savedStates = { ...defaultConfig, ...savedStatesRaw }; getCoPilotNewThreadAutoSubmitCheckbox().prop('checked', savedStates.coPilotNewThreadAutoSubmit); getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked', savedStates.coPilotRepeatLastAutoSubmit); getHideSideMenuCheckbox().prop('checked', savedStates.hideSideMenu); getTagsEnabledCheckbox().prop('checked', savedStates.tagsEnabled); 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); getTagContainerExtraBottomMarginInput().val(savedStates.tagContainerExtraBottomMargin); getTagLuminanceThresholdInput().val(savedStates.tagLuminanceThreshold); getTagBoldCheckbox().prop('checked', savedStates.tagBold); getTagItalicCheckbox().prop('checked', savedStates.tagItalic); getTagFontSizeInput().val(savedStates.tagFontSize); getTagIconSizeInput().val(savedStates.tagIconSize); getTagTextYOffsetInput().val(savedStates.tagTextYOffset); getTagIconYOffsetInput().val(savedStates.tagIconYOffset); getTagRoundnessInput().val(savedStates.tagRoundness); getReplaceIconsInMenu().val(savedStates.replaceIconsInMenu); getSlimLeftMenuCheckbox().prop('checked', savedStates.slimLeftMenu); getHideHomeWidgetsCheckbox().prop('checked', savedStates.hideHomeWidgets); getHideDiscoverButtonCheckbox().prop('checked', savedStates.hideDiscoverButton); getFixImageGenerationOverlayCheckbox().prop('checked', savedStates.fixImageGenerationOverlay); getExtraSpaceBellowLastAnswerCheckbox().prop('checked', savedStates.extraSpaceBellowLastAnswer); getModelLabelTextModeSelect().val(savedStates.modelLabelTextMode); getModelLabelStyleSelect().val(savedStates.modelLabelStyle); getModelLabelRemoveCpuIconCheckbox().prop('checked', savedStates.modelLabelRemoveCpuIcon); getModelLabelOverwriteCyanIconToGrayCheckbox().prop('checked', savedStates.modelLabelOverwriteCyanIconToGray); getModelLabelUseIconForReasoningModelsSelect().val(savedStates.modelLabelUseIconForReasoningModels ?? MODEL_LABEL_ICON_REASONING_MODEL.OFF); getModelLabelReasoningModelIconGoldCheckbox().prop('checked', savedStates.modelLabelReasoningModelIconGold); getCustomModelPopoverSelect().val(savedStates.customModelPopover ?? CUSTOM_MODEL_POPOVER_MODE.SIMPLE_GRID); getTagPaletteCustomTextArea().val((savedStates.tagPaletteCustom || []).join(', ')); getMainCaptionHtmlTextArea().val(savedStates.mainCaptionHtml); getCustomJsTextArea().val(savedStates.customJs); getCustomCssTextArea().val(savedStates.customCss); getCustomWidgetsHtmlTextArea().val(savedStates.customWidgetsHtml); getHideSideMenuLabels().prop('checked', savedStates.hideSideMenuLabels); }; function handleSettingsInit() { const modalExists = getPerplexityHelperModal().length > 0; const firstCheckboxExists = getCoPilotNewThreadAutoSubmitCheckbox().length > 0; if (!modalExists || firstCheckboxExists) { return; } const $tabButtons = $c(modalTabGroupTabsCls).addClass('flex gap-2 items-end'); const setActiveTab = (tabName) => { $c(modalTabGroupTabsCls).find('> button').each((_, tab) => { const $tab = jq(tab); if ($tab.attr('data-tab') === tabName) { $tab.addClass(modalTabGroupActiveCls); } else { $tab.removeClass(modalTabGroupActiveCls); } }); $c(modalTabGroupContentCls).each((_, tab) => { const $tab = jq(tab); if ($tab.attr('data-tab') === tabName) { $tab.show(); } else { $tab.hide(); } }); }; const createTabContent = (tabName, tabLabel) => { const $tabButton = jq('<button/>').text(tabLabel).attr('data-tab', tabName).on('click', () => setActiveTab(tabName)); $tabButtons.append($tabButton); const $tabContent = jq('<div/>') .addClass(modalTabGroupContentCls) .attr('data-tab', tabName); getSettingsModalContent().append($tabContent); return $tabContent; }; const insertSeparator = () => getSettingsLastTabGroupContent().append('<hr/>'); // ------------------------------------------------------------------------------------------------------------------- createTabContent('general', 'General'); createCheckbox(hideSideMenuCheckboxId, 'Hide Side Menu', saveConfigFromForm); createCheckbox(slimLeftMenuCheckboxId, 'Slim Left Menu', saveConfigFromForm); createCheckbox(hideHomeWidgetsCheckboxId, 'Hide Home Page Widgets', saveConfigFromForm); createCheckbox(hideSideMenuLabelsId, 'Hide Side Menu Labels', saveConfigFromForm); createCheckbox(hideDiscoverButtonCheckboxId, 'Hide Discover Button', saveConfigFromForm); createCheckbox(fixImageGenerationOverlayCheckboxId, 'Fix Image Generation Overlay Position (Experimental; only use if you encounter the submit button in a custom image prompt outside of the viewport)', saveConfigFromForm); createCheckbox(extraSpaceBellowLastAnswerCheckboxId, 'Add extra space bellow last answer', saveConfigFromForm); createSelect( replaceIconsInMenuId, 'Replace menu icons', Object.values(ICON_REPLACEMENT_MODE).map(value => ({ value, label: value })), () => { saveConfigFromForm(); replaceIconsInMenu(); } ); // ------------------------------------------------------------------------------------------------------------------- createTabContent('model', 'Model'); createSelect( modelLabelStyleSelectId, 'Model Label Style', Object.values(MODEL_LABEL_STYLE).map(value => ({ value, label: value })), saveConfigFromForm ); createSelect( modelLabelTextModeSelectId, 'Model Label Text', Object.values(MODEL_LABEL_TEXT_MODE).map(value => ({ value, label: value })), saveConfigFromForm ); createCheckbox(modelLabelOverwriteCyanIconToGrayCheckboxId, 'Overwrite Model Icon: Cyan -> Gray', saveConfigFromForm); createSelect( modelLabelUseIconForReasoningModelsSelectId, 'Use icon for reasoning models', Object.values(MODEL_LABEL_ICON_REASONING_MODEL).map(value => ({ value, label: value })), saveConfigFromForm ); createCheckbox(modelLabelReasoningModelIconGoldCheckboxId, 'Use gold color for reasoning model icon', saveConfigFromForm); createSelect( customModelPopoverSelectId, 'Custom Model Popover (Experimental)', Object.values(CUSTOM_MODEL_POPOVER_MODE).map(value => ({ value, label: value })), saveConfigFromForm ); createCheckbox(modelLabelRemoveCpuIconCheckboxId, 'Remove CPU icon', saveConfigFromForm); // ------------------------------------------------------------------------------------------------------------------- createTabContent('tags', 'Tags'); createCheckbox(tagsEnabledId, 'Enable Tags', saveConfigFromForm); createTextArea(tagsTextAreaId, 'Tags', saveConfigFromForm, tagsHelpText, [ { icon: 'l:images', tooltip: 'Lucide Icons', url: 'https://lucide.dev/icons' }, { icon: 'td:image', tooltip: 'TDesign Icons', url: 'https://tdesign.tencent.com/design/icon-en#header-69' } ]) .prop('rows', 12).css('min-width', '700px').prop('wrap', 'off'); const paletteLegendContainer = jq('<div/>').attr('id', 'palette-legend-container'); getSettingsLastTabGroupContent().append(paletteLegendContainer); const updatePaletteLegend = () => { paletteLegendContainer.empty().append(createPaletteLegend(loadConfig()?.tagPalette)); }; updatePaletteLegend(); createSelect( tagPaletteSelectId, 'Tag color palette', Object.keys(TAGS_PALETTES).map(key => ({ value: key, label: key })), () => { saveConfigFromForm(); updatePaletteLegend(); refreshTags(); } ); createTextArea( tagPaletteCustomTextAreaId, 'Custom Palette Colors (comma-separated):', () => { saveConfigFromForm(); // Update legend and tags only if CUSTOM is the selected palette if (getTagPaletteSelect().val() === TAGS_PALETTES.CUSTOM) { updatePaletteLegend(); refreshTags(); } } ).prop('rows', 2); // Make it a bit smaller than the main tags text area createTagsPreview(); 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 saveConfigFromFormAndForceRefreshTags = () => { saveConfigFromForm(); refreshTags({ force: true }); }; createCheckbox(tagBoldCheckboxId, 'Bold text', saveConfigFromFormAndForceRefreshTags); createCheckbox(tagItalicCheckboxId, 'Italic text', saveConfigFromFormAndForceRefreshTags); createNumberInput( tagFontSizeInputId, 'Font size', saveConfigFromFormAndForceRefreshTags, { min: 4, max: 64 } ); createNumberInput( tagIconSizeInputId, 'Icon size', saveConfigFromFormAndForceRefreshTags, { min: 4, max: 64 } ); createNumberInput( tagRoundnessInputId, 'Tag Roundness (px)', saveConfigFromFormAndForceRefreshTags, { min: 0, max: 32 } ); createNumberInput( tagTextYOffsetInputId, 'Text Y offset', saveConfigFromFormAndForceRefreshTags, { step: 1, min: -50, max: 50 } ); createNumberInput( tagIconYOffsetInputId, 'Icon Y offset', saveConfigFromFormAndForceRefreshTags, { step: 1, min: -50, max: 50 } ); createCheckbox(tagTweakNoBorderCheckboxId, 'No border', saveConfigFromFormAndForceRefreshTags); createCheckbox(tagTweakSlimPaddingCheckboxId, 'Slim padding', saveConfigFromFormAndForceRefreshTags); createCheckbox(tagTweakRichBorderColorCheckboxId, 'Rich Border Color', saveConfigFromFormAndForceRefreshTags); createCheckbox(tagTweakTextShadowCheckboxId, 'Text shadow', saveConfigFromFormAndForceRefreshTags); createNumberInput( tagLuminanceThresholdInputId, 'Tag Luminance Threshold (determines if tag is light or dark)', saveConfigFromFormAndForceRefreshTags, { step: 0.01, min: 0, max: 1 } ); createSelect( tagHomePageLayoutSelectId, 'Tag container layout on home page (requires page refresh)', Object.values(TAG_HOME_PAGE_LAYOUT).map(value => ({ value, label: value })), saveConfigFromForm ); createNumberInput( tagContainerExtraBottomMarginInputId, 'Extra bottom margin on home page (em)', saveConfigFromFormAndForceRefreshTags, { min: 0, max: 10, step: 0.5 } ); const $modelsList = jq('<div/>').text('Model IDs: '); const modelIds = PP.modelDescriptors.map(md => md.ppModelId).join(', '); $modelsList.append(modelIds); getSettingsLastTabGroupContent().append($modelsList); // ------------------------------------------------------------------------------------------------------------------- createTabContent('raw', 'Raw (HTML, CSS, JS)'); createTextArea(mainCaptionHtmlTextAreaId, 'Main Caption HTML', saveConfigFromForm) .prop('rows', 8).css('min-width', '700px'); createTextArea(customWidgetsHtmlTextAreaId, 'Custom Widgets HTML', saveConfigFromForm) .prop('rows', 8).css('min-width', '700px'); createTextArea(customCssTextAreaId, 'Custom CSS', saveConfigFromForm) .prop('rows', 8).css('min-width', '700px'); createTextArea(customJsTextAreaId, 'Custom JS', saveConfigFromForm) .prop('rows', 8).css('min-width', '700px'); // ------------------------------------------------------------------------------------------------------------------- createTabContent('settings', 'Settings'); getSettingsLastTabGroupContent().append(jq('<div/>').text('Settings are stored in your browser\'s local storage. It is recommended to backup your settings via the export button below after every change.')); const buttonsContainer = jq('<div/>').addClass('flex gap-2'); getSettingsLastTabGroupContent().append(buttonsContainer); const createExportButton = () => { const exportButton = jq('<button>') .text('Export Settings') .on('click', () => { const settings = JSON.stringify(getSavedStates(), null, 2); const blob = new Blob([settings], { type: 'application/json' }); const date = new Date().toISOString().replace(/[:]/g, '-').replace(/T/g, '--').split('.')[0]; // Format: YYYY-MM-DD--HH-MM-SS const filename = `perplexity-helper-settings_${date}.json`; const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); buttonsContainer.append(exportButton); }; createExportButton(); const createImportButton = () => { const importButton = jq('<button>') .text('Import Settings') .on('click', () => { const input = jq('<input type="file" accept=".json">'); input.on('change', async (event) => { const file = event.target.files[0]; if (file) { // this is a dangerous operation, so we need to confirm it const confirmOverwrite = confirm('This will overwrite your current settings. Do you want to continue?'); if (confirmOverwrite) { const reader = new FileReader(); reader.onload = (e) => { try { const settings = JSON.parse(e.target.result); saveConfig(settings); loadCurrentConfigToSettingsForm(); refreshTags(); alert('Settings imported successfully!'); } catch (error) { console.error('Error importing settings:', error); alert('Error importing settings. Please check the file format.'); } }; reader.readAsText(file); } } }); input.trigger('click'); }); buttonsContainer.append(importButton); }; createImportButton(); // ------------------------------------------------------------------------------------------------------------------- createTabContent('legacy', 'Legacy'); createCheckbox(coPilotNewThreadAutoSubmitCheckboxId, 'Auto Submit New Thread With CoPilot', saveConfigFromForm); createCheckbox(coPilotRepeatLastAutoSubmitCheckboxId, 'Auto Submit Repeat With CoPilot', saveConfigFromForm); // ------------------------------------------------------------------------------------------------------------------- createTabContent('debug', 'Debug'); // debug options at the bottom (do NOT add more normal options bellow this!) createCheckbox(enableDebugCheckboxId, 'Debug Mode', () => { saveConfigFromForm(); const checked = getEnableDebugCheckbox().prop('checked'); if (checked) { enableDebugMode(); } }); createCheckbox(enableTagsDebugCheckboxId, 'Debug Tags Mode', () => { saveConfigFromForm(); const checked = getEnableTagsDebugCheckbox().prop('checked'); if (checked) { enableTagsDebugging(); refreshTags(); } }); // ------------------------------------------------------------------------------------------------------------------- setActiveTab('general'); loadCurrentConfigToSettingsForm(); } 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 getSettingsModalContent = () => getPerplexityHelperModal().find(`.modal-content`); const getSettingsLastTabGroupContent = () => getSettingsModalContent().find(`.${modalTabGroupContentCls}`).last(); 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; // TODO: no longer used? was this for agentic questions? 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(), tagsEnabled: getTagsEnabledCheckbox().prop('checked'), 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(), tagContainerExtraBottomMargin: parseFloat(getTagContainerExtraBottomMarginInput().val()) || 0, tagLuminanceThreshold: parseFloat(getTagLuminanceThresholdInput().val()), tagBold: getTagBoldCheckbox().prop('checked'), tagItalic: getTagItalicCheckbox().prop('checked'), tagFontSize: parseInt(getTagFontSizeInput().val()), tagIconSize: parseInt(getTagIconSizeInput().val()), tagRoundness: parseInt(getTagRoundnessInput().val()), tagTextYOffset: parseInt(getTagTextYOffsetInput().val()), tagIconYOffset: parseInt(getTagIconYOffsetInput().val()), replaceIconsInMenu: getReplaceIconsInMenu().val(), slimLeftMenu: getSlimLeftMenuCheckbox().prop('checked'), hideSideMenuLabels: getHideSideMenuLabels().prop('checked'), hideHomeWidgets: getHideHomeWidgetsCheckbox().prop('checked'), hideDiscoverButton: getHideDiscoverButtonCheckbox().prop('checked'), fixImageGenerationOverlay: getFixImageGenerationOverlayCheckbox().prop('checked'), extraSpaceBellowLastAnswer: getExtraSpaceBellowLastAnswerCheckbox().prop('checked'), modelLabelTextMode: getModelLabelTextModeSelect().val(), modelLabelStyle: getModelLabelStyleSelect().val(), modelLabelOverwriteCyanIconToGray: getModelLabelOverwriteCyanIconToGrayCheckbox().prop('checked'), modelLabelUseIconForReasoningModels: getModelLabelUseIconForReasoningModelsSelect().val(), modelLabelReasoningModelIconGold: getModelLabelReasoningModelIconGoldCheckbox().prop('checked'), customModelPopover: getCustomModelPopoverSelect().val(), modelLabelRemoveCpuIcon: getModelLabelRemoveCpuIconCheckbox().prop('checked'), tagPaletteCustom: getTagPaletteCustomTextArea().val().split(',').map(s => s.trim()).filter(Boolean), mainCaptionHtml: getMainCaptionHtmlTextArea().val(), customJs: getCustomJsTextArea().val(), customCss: getCustomCssTextArea().val(), customWidgetsHtml: getCustomWidgetsHtmlTextArea().val(), }; saveConfig(checkBoxStates); }; const showPerplexityHelperSettingsModal = () => { loadCurrentConfigToSettingsForm(); getPerplexityHelperModal().show().css('display', 'flex'); }; const hidePerplexityHelperSettingsModal = () => { getPerplexityHelperModal().hide(); }; const handleTopSettingsButtonInsertion = () => { const copilotHelperSettings = getTopSettingsButtonEl(); // TODO: no longer works // 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'); showPerplexityHelperSettingsModal(); }); settingsButtonEl.attr('data-has-custom-click-event', true); } }; const applySideMenuHiding = () => { const config = loadConfigOrDefault(); if (!config.hideSideMenu) return; const $sideMenu = PP.getLeftPanel(); if ($sideMenu.hasClass(sideMenuHiddenCls)) return; $sideMenu.addClass(sideMenuHiddenCls); console.log(logPrefix, '[applySideMenuHiding] User requested hiding of side menu (left panel). You can open Perplexity Helper settings modal via typing (copy&paste):\n\nph.showPerplexityHelperSettingsModal()\n\nin Console in DevTools and executing via enter key.', { $sideMenu }); }; 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'); hidePerplexityHelperSettingsModal(); }); // Setup title animation setTimeout(() => { const $titleEl = getPerplexityHelperModal().find(`.${modalSettingsTitleCls}`); if ($titleEl.length) { const text = $titleEl.text(); const wrappedText = text .split('') .map((char, i) => { if (i === 0 || i === 11) { // P and H positions return `<span class="animate-letter" data-letter="${char}">${char}</span>`; } return char; }) .join(''); $titleEl.html(wrappedText); $titleEl.on('click', () => { const $firstLetter = $titleEl.find('.animate-letter').eq(0); const $secondLetter = $titleEl.find('.animate-letter').eq(1); // Staggered animation $firstLetter.addClass('active'); setTimeout(() => { $firstLetter.removeClass('active'); $secondLetter.addClass('active'); setTimeout(() => { $secondLetter.removeClass('active'); }, 500); }, 250); }); } }, 500); }; const lucideIconMappings = { LUCIDE1: leftPanelIconMappingsToLucide1, LUCIDE2: leftPanelIconMappingsToLucide2, }; const findKeyByValue = (obj, value) => Object.keys(obj).find(key => obj[key] === value); const SUPPORTED_ICON_REPLACEMENT_MODES = [ ICON_REPLACEMENT_MODE.LUCIDE1, ICON_REPLACEMENT_MODE.LUCIDE2, ICON_REPLACEMENT_MODE.LUCIDE3, ICON_REPLACEMENT_MODE.TDESIGN1, ICON_REPLACEMENT_MODE.TDESIGN2, ICON_REPLACEMENT_MODE.TDESIGN3, ]; const replaceIconsInMenu = () => { const config = loadConfigOrDefault(); const replacementMode = findKeyByValue(ICON_REPLACEMENT_MODE, config.replaceIconsInMenu); if (SUPPORTED_ICON_REPLACEMENT_MODES.includes(config.replaceIconsInMenu)) { const processedAttr = `data-${pplxHelperTag}-processed`; const iconMapping = iconMappings[replacementMode]; if (!iconMapping) { console.error(logPrefix, '[replaceIconsInMenu] iconMapping not found', { config, iconMappings }); return; } const $iconButtons = PP.getIconsInLeftPanel().find('a:has(> div.grid > svg)'); // debugLog('[replaceIconsInMenu] svgEls', svgEls); $iconButtons.each((idx, rawIconButton) => { const $iconButton = jq(rawIconButton); const $svg = $iconButton.find('svg'); const processed = $iconButton.attr(processedAttr); if (processed) return; if ($iconButton.attr('id') === leftSettingsButtonId) return; const iconName = dropStr(1)($iconButton.attr('href')) || 'search'; debugLog('[replaceIconsInMenu] iconName', iconName); const replacementIconName = iconMapping[iconName]; debugLog('[replaceIconsInMenu] replacementIconName', replacementIconName); $iconButton.attr(processedAttr, true); if (replacementIconName) { const isTDesign = config.replaceIconsInMenu.startsWith('TDesign'); const newIconUrl = (isTDesign ? getTDesignIconUrl : getLucideIconUrl)(replacementIconName); debugLog('[replaceIconsInMenu] replacing icon', { iconName, replacementIconName, $svg, newIconUrl }); $svg.hide(); const newIconEl = jq('<img>') .attr('src', newIconUrl) .addClass('invert opacity-50') .addClass('relative duration-150 [grid-area:1/-1] group-hover:scale-110 text-text-200') ; if (isTDesign) newIconEl.addClass('h-6'); $svg.parent().addClass(lucideIconParentCls); $svg.after(newIconEl); } else { if (!['plus', 'thread'].includes(iconName)) { console.error('[replaceIconsInMenu] no replacement icon found', { iconName, replacementIconName }); } } }); } }; const createSidebarButton = (options) => { const { svgHtml, label, testId, href } = options; return jq('<a>', { 'data-testid': testId, 'class': 'p-sm group flex w-full flex-col items-center justify-center gap-0.5', 'href': href ?? '#', }).append( jq('<div>', { 'class': 'grid size-[40px] place-items-center border-borderMain/50 ring-borderMain/50 divide-borderMain/50 dark:divide-borderMainDark/50 dark:ring-borderMainDark/50 dark:border-borderMainDark/50 bg-transparent' }).append( jq('<div>', { 'class': 'size-[90%] rounded-md duration-150 [grid-area:1/-1] group-hover:opacity-100 opacity-0 border-borderMain/50 ring-borderMain/50 divide-borderMain/50 dark:divide-borderMainDark/50 dark:ring-borderMainDark/50 dark:border-borderMainDark/50 bg-offsetPlus dark:bg-offsetPlusDark' }), jq(svgHtml).addClass('relative duration-150 [grid-area:1/-1] group-hover:scale-110 text-text-200'), ), jq('<div>', { 'class': 'light font-sans text-xs text-textOff dark:text-textOffDark selection:bg-super/50 selection:text-textMain dark:selection:bg-superDuper/10 dark:selection:text-superDark text-center', 'text': label ?? 'MISSING LABEL' }) ); } 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 = PP.getIconsInLeftPanel(); if ($leftPanel.length === 0) { debugLog('handleLeftSettingsButtonSetup: leftPanel not found'); } const $sidebarButton = createSidebarButton({ svgHtml: cogIco, label: 'Perplexity Helper', testId: 'perplexity-helper-settings', href: '#', }) .attr('id', leftSettingsButtonId) .on('click', () => { debugLog('left settings button clicked'); if (!PP.isBreakpoint('md')) { PP.getLeftPanel().hide(); } showPerplexityHelperSettingsModal(); }); $leftPanel.append($sidebarButton); }; const handleSlimLeftMenu = () => { const config = loadConfigOrDefault(); if (!config.slimLeftMenu) return; const $leftPanel = PP.getLeftPanel(); if ($leftPanel.length === 0) { // debugLog('handleSlimLeftMenu: leftPanel not found'); } $leftPanel.addClass(leftPanelSlimCls); $leftPanel.find('.py-md').css('width', '45px'); }; const handleHideHomeWidgets = () => { const config = loadConfigOrDefault(); if (!config.hideHomeWidgets) return; const homeWidgets = PP.getHomeWidgets(); if (homeWidgets.length === 0) { debugLog('handleHideHomeWidgets: homeWidgets not found'); return; } if (homeWidgets.length > 1) { console.warn(logPrefix, '[handleHideHomeWidgets] too many homeWidgets found', homeWidgets); } homeWidgets.hide(); }; const handleFixImageGenerationOverlay = () => { const config = loadConfigOrDefault(); if (!config.fixImageGenerationOverlay) return; const imageGenerationOverlay = PP.getImageGenerationOverlay(); if (imageGenerationOverlay.length === 0) { // debugLog('handleFixImageGenerationOverlay: imageGenerationOverlay not found'); return; } // only if wrench button is cyan (we are in custom prompt) if (!imageGenerationOverlay.find('button').hasClass('bg-super')) return; const transform = imageGenerationOverlay.css('transform'); if (!transform) return; // Handle both matrix and translate formats const matrixMatch = transform.match(/matrix\(.*,\s*([\d.]+),\s*([\d.]+)\)/); const translateMatch = transform.match(/translate\(([\d.]+)px(?:,\s*([\d.]+)px)?\)/); const currentX = matrixMatch ? matrixMatch[1] // Matrix format: 5th value is X translation : translateMatch?.[1] || 0; // Translate format: first value debugLog('[handleFixImageGenerationOverlay] currentX', currentX, 'transform', transform); imageGenerationOverlay.css({ transform: `translate(${currentX}px, 0px)` }); }; const handleExtraSpaceBellowLastAnswer = () => { const config = loadConfigOrDefault(); if (!config.extraSpaceBellowLastAnswer) return; jq('body') .find(`.erp-sidecar\\:h-fit .md\\:pt-md.isolate > .max-w-threadContentWidth`) .last() .css({ // backgroundColor: 'magenta', paddingBottom: '15em', }) ; }; const handleSearchPage = () => { 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 getLabelFromModelDescription = modelLabelStyle => modelLabelFromAriaLabel => modelDescription => { if (!modelDescription) return modelLabelFromAriaLabel; switch (modelLabelStyle) { case MODEL_LABEL_TEXT_MODE.FULL_NAME: return modelDescription.nameEn; case MODEL_LABEL_TEXT_MODE.SHORT_NAME: return modelDescription.nameEnShort ?? modelDescription.nameEn; case MODEL_LABEL_TEXT_MODE.PP_MODEL_ID: return modelDescription.ppModelId; case MODEL_LABEL_TEXT_MODE.OWN_NAME_VERSION_SHORT: const nameText = modelDescription.ownNameEn ?? modelDescription.nameEn; const versionTextRaw = modelDescription.ownVersionEnShort ?? modelDescription.ownVersionEn; const versionText = versionTextRaw?.replace(/ P$/, ' Pro'); // HACK: Gemini 2.5 Pro return [nameText, versionText].filter(Boolean).join(modelDescription.ownNameVersionSeparator ?? ' '); default: throw new Error(`Unknown model label style: ${modelLabelStyle}`); } }; const getExtraClassesFromModelLabelStyle = modelLabelStyle => { switch (modelLabelStyle) { case MODEL_LABEL_STYLE.BUTTON_SUBTLE: return modelLabelStyleButtonSubtleCls; case MODEL_LABEL_STYLE.BUTTON_WHITE: return modelLabelStyleButtonWhiteCls; case MODEL_LABEL_STYLE.BUTTON_CYAN: return modelLabelStyleButtonCyanCls; default: return ''; } }; const handleModelLabel = () => { const config = loadConfigOrDefault(); if (!config.modelLabelStyle || config.modelLabelStyle === MODEL_LABEL_STYLE.OFF) return; const $modelIcons = PP.getAnyModelButton(); $modelIcons.each((_, el) => { const $el = jq(el); if (!$el.find(`.${modelLabelCls}`).length) { $el.prepend(jq(`<span class="${modelLabelCls}"></span>`)); } if (!$el.hasClass(modelIconButtonCls)) { $el.addClass(modelIconButtonCls); } if (config.modelLabelRemoveCpuIcon) { $el.addClass(modelLabelRemoveCpuIconCls); } const $label = $el.find(`.${modelLabelCls}`); const modelDescription = PP.getModelDescriptionFromModelButton($el); const modelLabelFromAriaLabel = $el.attr('aria-label'); const modelLabel = getLabelFromModelDescription(config.modelLabelTextMode)(modelLabelFromAriaLabel)(modelDescription); if (!modelLabel) { console.error('[handleModelLabel] modelLabel is null', { modelDescription, modelLabelFromAriaLabel, $el }); return; } const extraClasses = [ getExtraClassesFromModelLabelStyle(config.modelLabelStyle), config.modelLabelOverwriteCyanIconToGray ? modelLabelOverwriteCyanIconToGrayCls : '', ].filter(Boolean).join(' '); $label.attr('data-extra-classes', extraClasses); const isReasoningModel = modelDescription?.modelType === 'reasoning'; const hasReasoningIcon = isReasoningModel && config.modelLabelUseIconForReasoningModels !== MODEL_LABEL_ICON_REASONING_MODEL.OFF; const reasoningIconRemoved = !isReasoningModel && config.modelLabelUseIconForReasoningModels !== MODEL_LABEL_ICON_REASONING_MODEL.OFF; const chipIconRemoved = config.modelLabelRemoveCpuIcon; if ($label.text() !== modelLabel) { $label.attr('data-model-description', JSON.stringify(modelDescription)); $label.text(modelLabel); $label.addClass(extraClasses); } // Reasoning model icon if (config.modelLabelUseIconForReasoningModels !== MODEL_LABEL_ICON_REASONING_MODEL.OFF) { const prevReasoningModelIcon = $el.find(`.${reasoningModelCls}`); const noReasoningIconAndModelIsNotReasoningModel = prevReasoningModelIcon.length === 0 && !isReasoningModel; const reasoningIconAndModelIsReasoningModel = prevReasoningModelIcon.length > 0 && isReasoningModel; if (noReasoningIconAndModelIsNotReasoningModel || reasoningIconAndModelIsReasoningModel) { if (noReasoningIconAndModelIsNotReasoningModel) { $el.addClass(notReasoningModelCls); } if (reasoningIconAndModelIsReasoningModel) { $el.removeClass(notReasoningModelCls); } return; } if (prevReasoningModelIcon.length === 0) { const iconUrl = getLucideIconUrl(config.modelLabelUseIconForReasoningModels.toLowerCase()); const $icon = jq(`<img src="${iconUrl}" alt="Reasoning model" class="${reasoningModelCls}" />`); if (config.modelLabelReasoningModelIconGold) { $icon.addClass(iconColorGoldCls); } $el.prepend($icon); } const $reasoningModelIcon = $el.find(`.${reasoningModelCls}`); if (isReasoningModel) { $reasoningModelIcon.css({ display: 'inline-block' }); if (config.modelLabelStyle === MODEL_LABEL_STYLE.JUST_TEXT) { $reasoningModelIcon.addClass(iconColorGrayCls); } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_CYAN) { $reasoningModelIcon.addClass(iconColorCyanCls); } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_SUBTLE) { $reasoningModelIcon.addClass(iconColorGrayCls); } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_WHITE) { $reasoningModelIcon.addClass(iconColorWhiteCls); } } else { $reasoningModelIcon.remove(); } } // End of reasoning model icon }); }; const handleHideDiscoverButton = () => { const config = loadConfigOrDefault(); if (!config.hideDiscoverButton) return; const $iconsInLeftPanel = PP.getIconsInLeftPanel().find('a[href="/discover"]'); $iconsInLeftPanel.hide(); }; const handleCustomModelPopover = () => { const config = loadConfigOrDefault(); const mode = config.customModelPopover; if (mode === CUSTOM_MODEL_POPOVER_MODE.OFF) return; const $modelSelectionList = PP.getModelSelectionList(); if ($modelSelectionList.length === 0) return; const processedAttr = 'ph-processed-custom-model-popover'; if ($modelSelectionList.attr(processedAttr)) return; $modelSelectionList.attr(processedAttr, true); $modelSelectionList.nthParent(2).css({ maxHeight: 'initial' }); const $reasoningDelim = $modelSelectionList.children(".sm\\:px-sm.relative"); const markListItemAsReasoningModel = (el) => { const $el = jq(el); const $icon = jq('<img>', { src: getLucideIconUrl(config.modelLabelUseIconForReasoningModels.toLowerCase()), alt: 'Reasoning model', class: reasoningModelCls, }).css({ marginLeft: '0px' }); $el.find('.default,.super').parent().find('>.font-sans').first().prepend($icon); }; const modelSelectionListType = PP.getModelSelectionListType($modelSelectionList); if (config.modelLabelUseIconForReasoningModels !== MODEL_LABEL_ICON_REASONING_MODEL.OFF) { if (modelSelectionListType === 'new') { const $delimIndex = $modelSelectionList.children().index($reasoningDelim); $modelSelectionList.children().slice($delimIndex + 1).each((_idx, el) => { markListItemAsReasoningModel(el); }); } else { $modelSelectionList .children() .filter((_idx, rEl) => jq(rEl).find('span').text().includes('Reasoning')) .each((_idx, el) => markListItemAsReasoningModel(el)); } } const $delims = $modelSelectionList.children(".sm\\:mx-sm"); const removeAllDelims = () => { $delims.hide(); $reasoningDelim.hide(); }; const removeAllModelDescriptions = () => { $modelSelectionList.find('div.light.text-textOff').hide(); $modelSelectionList.find('.group\\/item > .relative > .gap-sm').css({ alignItems: 'center' }); }; if (mode === CUSTOM_MODEL_POPOVER_MODE.COMPACT_LIST) { removeAllDelims(); removeAllModelDescriptions(); return; } if (mode === CUSTOM_MODEL_POPOVER_MODE.SIMPLE_LIST) { // it is already a list, we forced the height to grow return; } $modelSelectionList.css({ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: mode === CUSTOM_MODEL_POPOVER_MODE.COMPACT_GRID ? '0px' : '10px', 'grid-auto-rows': 'min-content', }); if (mode === CUSTOM_MODEL_POPOVER_MODE.COMPACT_GRID) { removeAllDelims(); removeAllModelDescriptions(); } $delims.hide(); $reasoningDelim.css({ gridColumn: 'span 2', }); }; const mainCaptionAppliedCls = genCssName('mainCaptionApplied'); const handleMainCaptionHtml = () => { const config = loadConfigOrDefault(); if (!config.mainCaptionHtml) return; if (PP.getMainCaption().hasClass(mainCaptionAppliedCls)) return; PP.setMainCaptionHtml(config.mainCaptionHtml); PP.getMainCaption().addClass(mainCaptionAppliedCls); }; const handleCustomJs = () => { const config = loadConfigOrDefault(); if (!config.customJs) return; try { // Use a static key to ensure we only run once per page load const dataKey = 'data-' + genCssName('custom-js-applied'); if (!jq('body').attr(dataKey)) { jq('body').attr(dataKey, true); // Use Function constructor to evaluate the JS code const customJsFn = new Function(config.customJs); customJsFn(); } } catch (error) { console.error('Error executing custom JS:', error); } }; const handleCustomCss = () => { const config = loadConfigOrDefault(); if (!config.customCss) return; try { // Check if custom CSS has already been applied const dataKey = 'data-' + genCssName('custom-css-applied'); if (!jq('head').attr(dataKey)) { jq('head').attr(dataKey, true); const styleElement = jq('<style></style>') .addClass(customCssAppliedCls) .text(config.customCss); jq('head').append(styleElement); } } catch (error) { console.error('Error applying custom CSS:', error); } }; const handleCustomWidgetsHtml = () => { const config = loadConfigOrDefault(); if (!config.customWidgetsHtml) return; try { // Check if custom widgets have already been applied const dataKey = 'data-' + genCssName('custom-widgets-html-applied'); if (!jq('body').attr(dataKey)) { jq('body').attr(dataKey, true); const widgetContainer = jq('<div></div>') .addClass(customWidgetsHtmlAppliedCls) .html(config.customWidgetsHtml); PP.getPromptAreaWrapperOfNewThread().after(widgetContainer); } } catch (error) { console.error('Error applying custom widgets HTML:', error); } }; const handleHideSideMenuLabels = () => { const config = loadConfigOrDefault(); if (!config.hideSideMenuLabels) return; const $sideMenu = PP.getLeftPanel(); if ($sideMenu.hasClass(sideMenuLabelsHiddenCls)) return; $sideMenu.addClass(sideMenuLabelsHiddenCls); }; const work = () => { handleModalCreation(); handleTopSettingsButtonInsertion(); handleTopSettingsButtonSetup(); handleSettingsInit(); handleLeftSettingsButtonSetup(); handleExtraSpaceBellowLastAnswer(); handleHideDiscoverButton(); handleHideSideMenuLabels(); 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) { handleSearchPage(); } }; const fastWork = () => { handleCustomModelPopover(); handleSlimLeftMenu(); handleHideHomeWidgets(); applySideMenuHiding(); replaceIconsInMenu(); handleModelLabel(); handleMainCaptionHtml(); handleCustomJs(); handleCustomCss(); handleCustomWidgetsHtml(); }; 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', Inter: 'https://cdn.jsdelivr.net/npm/@fontsource/[email protected]/index.min.css', }; 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); } }; const setupFixImageGenerationOverlay = () => { const config = loadConfigOrDefault(); if (config.fixImageGenerationOverlay) { setInterval(handleFixImageGenerationOverlay, 250); } }; (function () { if (loadConfigOrDefault()?.enableDebug) { enableDebugMode(); } debugLog('TAGS_PALETTES', TAGS_PALETTES); if (loadConfigOrDefault()?.debugTagsMode) { enableTagsDebugging(); } 'use strict'; jq("head").append(`<style>${styles}</style>`); setupTags(); setupFixImageGenerationOverlay(); const mainInterval = setInterval(work, 1000); const fastInterval = setInterval(fastWork, 100); window.ph = { stopWork: () => { clearInterval(mainInterval); clearInterval(fastInterval); }, work, fastWork, jq, showPerplexityHelperSettingsModal, enableTagsDebugging: () => { debugTags = true; }, disableTagsDebugging: () => { debugTags = false; }, }; loadFont(loadConfigOrDefault().tagFont); console.log(`%c${userscriptName}%c\n %cTiartyos%c & %cmonnef%c\n ... loaded`, 'color: #aaffaa; font-size: 1.5rem; background-color: rgba(0, 0, 0, 0.5); padding: 2px;', '', 'color: #6b02ff; font-weight: bold; background-color: rgba(0, 0, 0, 0.5); padding: 2px;', '', 'color: #aa2cc3; font-weight: bold; background-color: rgba(0, 0, 0, 0.5); padding: 2px;', '', ''); console.log('to show settings use:\nph.showPerplexityHelperSettingsModal()'); }());