您需要先安装一个扩展,例如 篡改猴、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 6.3 // @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/umd/hex-to-css-filter.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== if (EventTarget.prototype.original_addEventListener == null) { EventTarget.prototype.original_addEventListener = EventTarget.prototype.addEventListener; const interestedIn = { typSubstring: [ 'key', ], nodeName: [ // 'TEXTAREA', // 'INPUT', // 'BODY', ], } function addEventListener_hook(typ, fn, opt) { // console.log('--- add event listener', { nodeName: this.nodeName, typ, fn, opt, el: this }); this.all_handlers = this.all_handlers || []; this.all_handlers.push({ typ, fn, opt }); this.original_addEventListener(typ, fn, opt); if (interestedIn.typSubstring.some(s => typ.includes(s)) || interestedIn.nodeName.includes(this.nodeName)) { debugLog('!!! added event listener', { typ, fn, opt, el: this }); } } EventTarget.prototype.addEventListener = addEventListener_hook; } const PP = window.PP.noConflict(); const jq = PP.jq; const hexToCssFilter = window.HexToCSSFilter.hexToCSSFilter; const $c = (cls, parent) => jq(`.${cls}`, parent); const $i = (id, parent) => jq(`#${id}`, parent); const takeStr = n => str => str.slice(0, n); const dropStr = n => str => str.slice(n); const dropRightStr = n => str => str.slice(0, -n); const filter = pred => xs => xs.filter(pred); const pipe = x => (...fns) => fns.reduce((acc, fn) => fn(acc), x); const nl = '\n'; const markdownConverter = new showdown.Converter({ tables: true }); let debugMode = false; const enableDebugMode = () => { debugMode = true; }; const userscriptName = 'Perplexity helper'; const logPrefix = `[${userscriptName}]`; const debugLog = (...args) => { if (debugMode) { console.debug(logPrefix, ...args); } }; let debugTags = false; const debugLogTags = (...args) => { if (debugTags) { console.debug(logPrefix, '[tags]', ...args); } }; const log = (...args) => { console.log(logPrefix, ...args); }; const logError = (...args) => { console.error(logPrefix, ...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 getBrandIconInfo = (modelName = '') => { const normalizedModelName = modelName.toLowerCase(); if (normalizedModelName.includes('claude')) { return { iconName: 'claude', brandColor: '#D97757' }; } else if (normalizedModelName.includes('gpt') || normalizedModelName.startsWith('o')) { return { iconName: 'openai', brandColor: '#FFFFFF' }; } else if (normalizedModelName.includes('gemini')) { return { iconName: 'gemini', brandColor: '#1C69FF' }; } else if (normalizedModelName.includes('sonar') || normalizedModelName.includes('r1') || normalizedModelName.includes('best') || normalizedModelName.includes('auto')) { return { iconName: 'perplexity', brandColor: '#22B8CD' }; } else if (normalizedModelName.includes('grok')) { return { iconName: 'xai', brandColor: '#FFFFFF' }; } else if (normalizedModelName.includes('llama') || normalizedModelName.includes('meta')) { return { iconName: 'meta', brandColor: '#1D65C1' }; } else if (normalizedModelName.includes('anthropic')) { return { iconName: 'anthropic', brandColor: '#F1F0E8' }; } return null; }; const getTDesignIconUrl = iconName => `https://api.iconify.design/tdesign:${iconName}.svg`; const getLobeIconsUrl = iconName => `https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@latest/icons/${iconName}.svg`; const parseIconName = iconName => { if (!iconName.includes(':')) return { typePrefix: 'l', processedIconName: iconName }; const [typePrefix, processedIconName] = iconName.split(':'); return { typePrefix, processedIconName }; }; const getIconUrl = iconName => { const { typePrefix, processedIconName } = parseIconName(iconName); if (typePrefix === 'td') { return getTDesignIconUrl(processedIconName); } if (typePrefix === 'l') { return getLucideIconUrl(processedIconName); } throw new Error(`Unknown icon type: ${typePrefix}`); }; const pplxHelperTag = 'pplx-helper'; const genCssName = x => `${pplxHelperTag}--${x}`; const button = (id, icoName, title, extraClass) => `<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 modelLabelLargerIconsCls = genCssName('model-label-larger-icons'); const notReasoningModelCls = genCssName('not-reasoning-model'); const modelIconCls = genCssName('model-icon'); const iconColorCyanCls = genCssName('icon-color-cyan'); const iconColorGrayCls = genCssName('icon-color-gray'); const iconColorWhiteCls = genCssName('icon-color-white'); const errorIconCls = genCssName('error-icon'); 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 leftMarginOfThreadContentStylesId = genCssName('left-margin-of-thread-content-styles'); const enhancedSubmitButtonCls = genCssName('enhanced-submit-button'); const enhancedSubmitButtonPhTextCls = genCssName('enhanced-submit-button-ph-text'); const enhancedSubmitButtonActiveCls = genCssName('enhanced-submit-button-active'); // Added proper generated class name const promptAreaKeyListenerCls = genCssName('prompt-area-key-listener'); const promptAreaKeyListenerIndicatorCls = genCssName('prompt-area-key-listener-indicator'); const pulseFocusCls = genCssName('pulse-focus'); 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; */ padding-top: 3px; /*margin-right: 0.5em;*/ } 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-left: 0.5em; */ padding-right: 1.25em; } .${modelIconCls} { width: 16px; min-width: 16px; height: 16px; margin-right: 2px; margin-left: 0; margin-top: -0px; opacity: 0.5; transition: opacity 0.2s; } button.${modelLabelLargerIconsCls} .${modelIconCls} { transform: scale(1.2); } button:hover .${modelIconCls} { opacity: 1; } 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; } /* Apply style even if the span is empty */ button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}:empty) { border: 1px solid #333; } button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonSubtleCls}:empty):hover { 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; } /* Apply style even if the span is empty */ button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}:empty) { background: #191A1A !important; color: #2D2F2F !important; } button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonWhiteCls}:empty):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}; } /* Apply style even if the span is empty */ button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}:empty) { border: 1px solid ${cyanMediumPerplexityColor}; background: ${cyanDarkPerplexityColor} !important; } button.${modelIconButtonCls}:has(> .${modelLabelCls}.${modelLabelStyleButtonCyanCls}:empty):hover { 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: 2px; margin-left: 2px; */ margin-top: -2px; filter: invert(); opacity: 0.5; transition: opacity 0.2s; } button.${modelLabelLargerIconsCls} .${reasoningModelCls} { transform: scale(1.2); } button:hover .${reasoningModelCls} { opacity: 1; } .${errorIconCls} { width: 16px; height: 16px; margin-right: 4px; margin-left: 4px; margin-top: -0px; opacity: 0.75; transition: opacity 0.2s; } button.${modelLabelLargerIconsCls} .${errorIconCls} { transform: scale(1.2); } button:hover .${errorIconCls} { opacity: 1; } /* 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(100%); opacity: 0.5; 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%); } .${sideMenuHiddenCls} { display: none; } .${sideMenuLabelsHiddenCls} .p-sm > div.font-sans.text-xs { display: none; } .${enhancedSubmitButtonCls} { position: absolute; top: 0; left: 0; width: 101%; height: 101%; border-radius: inherit; cursor: pointer; background: transparent; box-shadow: 0 0 0 1px transparent; z-index: 10; display: flex; align-items: center; justify-content: center; opacity: 0; transform: scale(1.1); transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); overflow: visible; pointer-events: none; } /* ISSUE: Using hard-coded 'active' class here instead of enhancedSubmitButtonActiveCls */ .${enhancedSubmitButtonCls}.active { opacity: 0.5; transform: scale(1); pointer-events: auto; box-shadow: 0 0 0 1px cyan inset; } .${enhancedSubmitButtonCls}:hover { opacity: 1; background: radial-gradient(circle at right top, rgb(23, 8, 56), rgb(4, 2, 12)); } .${enhancedSubmitButtonCls}::before { content: ''; position: absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; background: transparent; z-index: -1; box-shadow: 0 0 0 1.2px transparent; border-radius: inherit; transition: opacity 0.4s ease-in-out, box-shadow 0.4s ease-in-out; opacity: 0; } /* ISSUE: Using hard-coded 'active' class here instead of enhancedSubmitButtonActiveCls */ .${enhancedSubmitButtonCls}.active::before { opacity: 0.9; box-shadow: 0 0 0 1.2px #00ffff; animation: gradientBorder 3s ease infinite; } .${enhancedSubmitButtonCls}:hover::before { opacity: 1; } @keyframes gradientBorder { 0% { box-shadow: 0 0 0 1.2px rgba(0, 255, 255, 0.6); } 50% { box-shadow: 0 0 0 1.2px rgba(0, 255, 255, 1), 0 0 8px rgba(0, 255, 255, 0.6); } 100% { box-shadow: 0 0 0 1.2px rgba(0, 255, 255, 0.6); } } @keyframes pulseIndicator { 0% { transform: scale(1); opacity: 0.6; } 50% { transform: scale(1.5); opacity: 1; } 100% { transform: scale(1); opacity: 0.6; } } .${enhancedSubmitButtonPhTextCls} { font-family: 'JetBrains Mono', monospace; color: #00c1ff; display: none; position: absolute; font-size: 20px; user-select: none; align-items: center; justify-content: center; width: 100%; height: 100%; } .${enhancedSubmitButtonCls}:hover .${enhancedSubmitButtonPhTextCls} { display: flex; } /* Prompt area with active toggle tags */ textarea.${promptAreaKeyListenerCls} { box-shadow: 0 0 0 1px rgba(31, 184, 205, 0.2), 0 0px 0px rgba(31, 184, 205, 0); transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); border-color: rgba(31, 184, 205, 0.2); position: relative; background-image: linear-gradient(to bottom, rgba(31, 184, 205, 0.03), transparent); } /* Nice glow effect when focused */ textarea.${promptAreaKeyListenerCls}:focus { box-shadow: 0 0 0 1px rgba(31, 184, 205, 0.5), 0 0 8px 1px rgba(31, 184, 205, 0.3); border-color: rgba(31, 184, 205, 0.5); background-image: linear-gradient(to bottom, rgba(31, 184, 205, 0.05), transparent); } /* Active indicator for textarea */ .${promptAreaKeyListenerIndicatorCls} { position: absolute; bottom: 5px; right: 5px; width: 4px; height: 4px; border-radius: 50%; background-color: rgba(31, 184, 205, 0.6); z-index: 5; pointer-events: none; box-shadow: 0 0 4px 1px rgba(31, 184, 205, 0.4); animation: pulseIndicator 2s ease-in-out infinite; opacity: 0; transform: scale(0); transition: opacity 0.5s ease, transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); } /* When actually visible, override initial zero values */ .${promptAreaKeyListenerIndicatorCls}.visible { opacity: 1; transform: scale(1); } /* Pulse focus effect when Enter is pressed */ textarea.${pulseFocusCls} { box-shadow: 0 0 0 2px rgba(31, 184, 205, 0.8), 0 0 12px 4px rgba(31, 184, 205, 0.6) !important; border-color: rgba(31, 184, 205, 0.8) !important; transition: none !important; } `; 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. - \`toggle-mode\`: makes the tag work as a toggle button. When toggled on (highlighted), a special cyan/green outline appears around the submit button. Click this enhanced submit button to apply all toggled tag actions before submitting. Toggle status is saved between sessions. No parameters needed - just use \`<toggle-mode>\`. - \`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> <label:Sonnet><toggle-mode><set-model:claude-3-7-sonnet-thinking><icon:brain> <toggle-mode><label:Add Note><position:after><color:%2>\n\nNOTE: This is a toggle-mode note appended to the end of prompt \`\`\` 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 === 'toggle-mode') 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|toggle-mode)(?::([^<>]*))?>/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; }; // Function to update a toggle tag's visual state const updateToggleTagState = (tagEl, tag, newToggleState) => { if (!tagEl || !tag) return; const isTagLight = color2k.getLuminance(tag.color) > loadConfigOrDefault().tagLuminanceThreshold; const colorMod = isTagLight ? color2k.darken : color2k.lighten; const hoverBgColor = color2k.toRgba(colorMod(tag.color, 0.1)); // For toggle tags, adjust the color based on toggle state const toggledColor = newToggleState ? color2k.lighten(tag.color, 0.3) : tag.color; // Update the tag element tagEl.attr('data-toggled', newToggleState); tagEl.css('background-color', toggledColor); tagEl.attr('data-hoverBgColor', color2k.toHex(hoverBgColor)); // Update tooltip if using default if (!tag.tooltip) { const newTooltip = `${logPrefix} Toggle ${newToggleState ? 'off' : 'on'} - ${tag.label || 'tag'}`; tagEl.prop('title', newTooltip); } }; const createTag = containerEl => isPreview => tag => { if (tag.hide) return null; // Generate a unique identifier for this toggle tag const tagId = generateToggleTagId(tag); // Get saved toggle state if this is a toggle-mode tag and tagToggleSave is enabled const config = loadConfigOrDefault(); // Make sure tagToggledStates exists to prevent errors if (!config.tagToggledStates) { config.tagToggledStates = {}; saveConfig(config); } // TODO: rewrite most of code with _phTagToggleState - new util functions/classes for working with it // Check both the in-memory toggle state and the saved toggle state (if tagToggleSave is enabled) // In-memory toggle state takes precedence during the current session const inMemoryToggleState = window._phTagToggleState && tagId ? window._phTagToggleState[tagId] : undefined; const savedToggleState = (tagId && config.tagToggleSave) ? config.tagToggledStates[tagId] || false : false; const isToggled = inMemoryToggleState !== undefined ? inMemoryToggleState : savedToggleState; 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 = async (evt) => { debugLog('clicked', tag, evt); if (tag.link) return; // Handle toggle mode if (tag.toggleMode) { const el = jq(evt.currentTarget); // Get the current toggle state directly from the element // This is critical for handling multiple clicks correctly const currentToggleState = el.attr('data-toggled') === 'true'; const newToggleState = !currentToggleState; // Update the toggle state in config only if tagToggleSave is enabled // Make sure tagId is valid before using it if (tagId) { const config = loadConfigOrDefault(); // Create a temporary in-memory toggle state for visual indication // We'll track this regardless of tagToggleSave setting window._phTagToggleState = window._phTagToggleState || {}; window._phTagToggleState[tagId] = newToggleState; // Only save the toggle state permanently if the tagToggleSave setting is enabled if (config.tagToggleSave) { const updatedConfig = { ...config, tagToggledStates: { ...config.tagToggledStates, [tagId]: newToggleState } }; saveConfig(updatedConfig); } // Update visual indicators for submit buttons updateToggleIndicators(); // Update the tag's visual state updateToggleTagState(el, tag, newToggleState); } else { debugLog('Error: Invalid toggle tag ID', tag); } return; } // Regular tag handling for non-toggle tags try { // Apply all tag's actions and wait for them to complete await applyTagActions(tag); // Handle auto submit for this tag after all actions are applied if (tag.autoSubmit) { 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'); } } else { // Focus the prompt area if we're not auto-submitting const el = jq(evt.currentTarget); const tagsContainer = el.closest(`.${tagsContainerCls}`); if (tagsContainer.length) { const promptArea = getPromptAreaFromTagsContainer(tagsContainer); if (promptArea.length) { promptArea[0].focus(); } } } } catch (error) { debugLog('Error applying tag actions:', error); } }; const tagFont = loadConfig().tagFont; // Create tooltip message based on tag type - without using let const tooltipMsg = tag.link ? `${logPrefix} Open link: ${tag.link}` : tag.toggleMode ? `${logPrefix} Toggle ${isToggled ? 'off' : 'on'} - ${tag.label || 'tag'}` : `${logPrefix} Insert \`${tag.text}\` at position \`${posFromTag(tag)}\``; const defaultTooltip = tooltipMsg; // For toggle tags, adjust the color based on toggle state const toggledColor = isToggled ? color2k.lighten(tag.color, 0.3) : tag.color; const backgroundColor = tag.toggleMode ? toggledColor : tag.color; const tagEl = jq(`<div/>`) .addClass(tagCls) .prop('title', tag.tooltip ?? defaultTooltip) .attr('data-tag', JSON.stringify(tag)) .css({ backgroundColor, borderColor, fontFamily: tagFont, borderRadius: `${loadConfig().tagRoundness}px`, }) .attr('data-color', color2k.toHex(tag.color)) .attr('data-hoverBgColor', color2k.toHex(hoverBgColor)) .attr('data-font', tagFont) .attr('data-toggled', isToggled.toString()) .on('mouseenter', event => { jq(event.currentTarget).css('background-color', hoverBgColor); }) .on('mouseleave', event => { const el = jq(event.currentTarget); const isCurrentToggled = el.attr('data-toggled') === 'true'; const currentColor = tag.toggleMode && isCurrentToggled ? color2k.lighten(tag.color, 0.3) : tag.color; el.css('background-color', currentColor); }); 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()) .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', VERY_SHORT: 'Very Short', FAMILIAR_NAME: 'Familiar Name', }); const MODEL_LABEL_STYLE = Object.freeze({ OFF: 'Off', NO_TEXT: 'No text', 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', COG: 'Cog', BRAIN_COG: 'Brain Cog', CALCULATOR: 'Calculator', BOT: 'Bot', }); const MODEL_LABEL_ICONS = Object.freeze({ OFF: 'Off', MONOCHROME: 'Monochrome', COLOR: 'Color', }); const defaultConfig = Object.freeze({ // General hideSideMenu: false, slimLeftMenu: false, hideSideMenuLabels: false, hideHomeWidgets: false, hideDiscoverButton: false, fixImageGenerationOverlay: false, extraSpaceBellowLastAnswer: false, replaceIconsInMenu: ICON_REPLACEMENT_MODE.OFF, leftMarginOfThreadContent: null, // Model modelLabelTextMode: MODEL_LABEL_TEXT_MODE.OFF, modelLabelStyle: MODEL_LABEL_STYLE.OFF, modelLabelOverwriteCyanIconToGray: false, modelLabelUseIconForReasoningModels: MODEL_LABEL_ICON_REASONING_MODEL.OFF, modelLabelReasoningModelIconColor: '#ffffff', modelLabelRemoveCpuIcon: false, modelLabelLargerIcons: false, modelLabelIcons: MODEL_LABEL_ICONS.OFF, 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, tagToggleSave: false, toggleModeHooks: true, tagToggleModeIndicators: true, tagToggledStates: {}, // Store toggle states by tag identifier // Raw mainCaptionHtml: '', mainCaptionHtmlEnabled: false, customJs: '', customJsEnabled: false, customCss: '', customCssEnabled: false, customWidgetsHtml: '', customWidgetsHtmlEnabled: false, // Settings activeSettingsTab: 'general', // Debug debugMode: false, debugTagsMode: false, debugTagsSuppressSubmit: false, autoOpenSettings: 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 debugTagsSuppressSubmitCheckboxId = genCssName('debugTagsSuppressSubmit'); const getDebugTagsSuppressSubmitCheckbox = () => $i(debugTagsSuppressSubmitCheckboxId); 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 tagToggleSaveCheckboxId = genCssName('tagToggleSave'); const getTagToggleSaveCheckbox = () => $i(tagToggleSaveCheckboxId); const toggleModeHooksCheckboxId = genCssName('toggleModeHooks'); const getToggleModeHooksCheckbox = () => $i(toggleModeHooksCheckboxId); const tagToggleModeIndicatorsCheckboxId = genCssName('tagToggleModeIndicators'); const getTagToggleModeIndicatorsCheckbox = () => $i(tagToggleModeIndicatorsCheckboxId); 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 leftMarginOfThreadContentInputId = genCssName('leftMarginOfThreadContent'); const getLeftMarginOfThreadContentInput = () => $i(leftMarginOfThreadContentInputId); 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 modelLabelReasoningModelIconColorId = genCssName('modelLabelReasoningModelIconColor'); const getModelLabelReasoningModelIconColor = () => $i(modelLabelReasoningModelIconColorId); const modelLabelRemoveCpuIconCheckboxId = genCssName('modelLabelRemoveCpuIconCheckbox'); const getModelLabelRemoveCpuIconCheckbox = () => $i(modelLabelRemoveCpuIconCheckboxId); const modelLabelLargerIconsCheckboxId = genCssName('modelLabelLargerIconsCheckbox'); const getModelLabelLargerIconsCheckbox = () => $i(modelLabelLargerIconsCheckboxId); const modelLabelIconsSelectId = genCssName('modelLabelIconsSelect'); const getModelLabelIconsSelect = () => $i(modelLabelIconsSelectId); 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 mainCaptionHtmlEnabledId = genCssName('mainCaptionHtmlEnabled'); const customJsEnabledId = genCssName('customJsEnabled'); const customCssEnabledId = genCssName('customCssEnabled'); const customWidgetsHtmlEnabledId = genCssName('customWidgetsHtmlEnabled'); const getMainCaptionHtmlEnabledCheckbox = () => $i(mainCaptionHtmlEnabledId); const getCustomJsEnabledCheckbox = () => $i(customJsEnabledId); const getCustomCssEnabledCheckbox = () => $i(customCssEnabledId); const getCustomWidgetsHtmlEnabledCheckbox = () => $i(customWidgetsHtmlEnabledId); const hideSideMenuLabelsId = genCssName('hideSideMenuLabels'); const getHideSideMenuLabels = () => $i(hideSideMenuLabelsId); const autoOpenSettingsCheckboxId = genCssName('autoOpenSettings'); const getAutoOpenSettingsCheckbox = () => $i(autoOpenSettingsCheckboxId); 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.debugMode); getEnableTagsDebugCheckbox().prop('checked', savedStates.debugTagsMode); getDebugTagsSuppressSubmitCheckbox().prop('checked', savedStates.debugTagsSuppressSubmit); 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); getTagToggleSaveCheckbox().prop('checked', savedStates.tagToggleSave); getToggleModeHooksCheckbox().prop('checked', savedStates.toggleModeHooks); getTagToggleModeIndicatorsCheckbox().prop('checked', savedStates.tagToggleModeIndicators); 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); getModelLabelLargerIconsCheckbox().prop('checked', savedStates.modelLabelLargerIcons); getModelLabelOverwriteCyanIconToGrayCheckbox().prop('checked', savedStates.modelLabelOverwriteCyanIconToGray); getModelLabelUseIconForReasoningModelsSelect().val(savedStates.modelLabelUseIconForReasoningModels ?? MODEL_LABEL_ICON_REASONING_MODEL.OFF); getModelLabelReasoningModelIconColor().val(savedStates.modelLabelReasoningModelIconColor || '#ffffff'); getModelLabelIconsSelect().val(savedStates.modelLabelIcons ?? MODEL_LABEL_ICONS.OFF); 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); getMainCaptionHtmlEnabledCheckbox().prop('checked', savedStates.mainCaptionHtmlEnabled); getCustomJsEnabledCheckbox().prop('checked', savedStates.customJsEnabled); getCustomCssEnabledCheckbox().prop('checked', savedStates.customCssEnabled); getCustomWidgetsHtmlEnabledCheckbox().prop('checked', savedStates.customWidgetsHtmlEnabled); getHideSideMenuLabels().prop('checked', savedStates.hideSideMenuLabels); getLeftMarginOfThreadContentInput().val(savedStates.leftMarginOfThreadContent); getAutoOpenSettingsCheckbox().prop('checked', savedStates.autoOpenSettings); }; 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(); } }); // Save the active tab to config const config = loadConfigOrDefault(); saveConfig({ ...config, activeSettingsTab: tabName }); }; 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(); } ); createNumberInput( leftMarginOfThreadContentInputId, 'Left margin of thread content (in em; empty for disabled; 0 for removing left whitespace in thread with normal sidebar width; -1 for slim sidebar)', saveConfigFromForm, { min: -10, max: 10, step: 0.5 } ); // ------------------------------------------------------------------------------------------------------------------- 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 ); createColorInput(modelLabelReasoningModelIconColorId, 'Color for reasoning model icon', saveConfigFromForm); createSelect( modelLabelIconsSelectId, 'Model Label Icons', Object.values(MODEL_LABEL_ICONS).map(value => ({ value, label: value })), 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); createCheckbox(modelLabelLargerIconsCheckboxId, 'Use larger model icons', 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); createCheckbox(tagToggleSaveCheckboxId, 'Save toggle-mode tag states', () => { const isEnabled = getTagToggleSaveCheckbox().prop('checked'); // If we're turning off the setting, reset saved toggle states if (!isEnabled) { const config = loadConfigOrDefault(); if (config.tagToggledStates && Object.keys(config.tagToggledStates).length > 0) { if (confirm('Do you want to clear all saved toggle states?')) { const updatedConfig = { ...config, tagToggledStates: {} }; saveConfig(updatedConfig); } } } saveConfigFromForm(); }); createCheckbox(toggleModeHooksCheckboxId, 'Toggle mode hooks (experimental)', saveConfigFromForm); createCheckbox(tagToggleModeIndicatorsCheckboxId, 'Toggle mode indicators', saveConfigFromForm); // Add a reset button for toggle states const resetToggleStatesButton = jq('<button>') .text('Reset All Toggle States') .on('click', () => { resetAllToggleStates(); }) .css({ marginLeft: '10px', marginBottom: '10px', padding: '3px 8px', fontSize: '0.9em' }); getSettingsLastTabGroupContent().append(resetToggleStatesButton); 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)'); createCheckbox(mainCaptionHtmlEnabledId, 'Enable Main Caption HTML', saveConfigFromForm); createTextArea(mainCaptionHtmlTextAreaId, 'Main Caption HTML', saveConfigFromForm) .prop('rows', 8).css('min-width', '700px'); insertSeparator(); createCheckbox(customWidgetsHtmlEnabledId, 'Enable Custom Widgets HTML', saveConfigFromForm); createTextArea(customWidgetsHtmlTextAreaId, 'Custom Widgets HTML', saveConfigFromForm) .prop('rows', 8).css('min-width', '700px'); insertSeparator(); createCheckbox(customCssEnabledId, 'Enable Custom CSS', saveConfigFromForm); createTextArea(customCssTextAreaId, 'Custom CSS', saveConfigFromForm) .prop('rows', 8).css('min-width', '700px'); insertSeparator(); createCheckbox(customJsEnabledId, 'Enable Custom JavaScript', saveConfigFromForm); 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('about', 'About'); getSettingsLastTabGroupContent().append(jq('<div/>').html(` Perplexity Helper is a userscript that adds many quality of life features to Perplexity.<br> <br> Maintainer: <a href="https://gitlab.com/monnef" target="_blank">monnef</a> <span class="opacity-50">(tags, model picker and labels, rewrite of settings)</span><br> Original author: <a href="https://gitlab.com/tiartyos" target="_blank">Tiartyos</a> <span class="opacity-50">(copilot buttons, basic settings)</span> `)); // ------------------------------------------------------------------------------------------------------------------- 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(); } }); createCheckbox(debugTagsSuppressSubmitCheckboxId, 'Debug: Suppress Submit After Applying Tags', saveConfigFromForm); createCheckbox(autoOpenSettingsCheckboxId, 'Automatically open settings after page load', saveConfigFromForm); getSettingsLastTabGroupContent().append(` <h2>Lobe Icons test</h2> <table style="border-collapse: separate; border-spacing: 20px; width: fit-content;"> <tr> <td>Default</td> <td><img src="${getLobeIconsUrl('anthropic')}"></td> </tr> <tr> <td>Default (inverted)</td> <td><img class="invert" src="${getLobeIconsUrl('anthropic')}"></td> </tr> </table> `); // ------------------------------------------------------------------------------------------------------------------- // Use the saved active tab if available, otherwise default to 'general' const config = loadConfigOrDefault(); setActiveTab(config.activeSettingsTab || defaultConfig.activeSettingsTab); 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 newConfig = { ...loadConfigOrDefault(), coPilotNewThreadAutoSubmit: getCoPilotNewThreadAutoSubmitCheckbox().prop('checked'), coPilotRepeatLastAutoSubmit: getCoPilotRepeatLastAutoSubmitCheckbox().prop('checked'), hideSideMenu: getHideSideMenuCheckbox().prop('checked'), slimLeftMenu: getSlimLeftMenuCheckbox().prop('checked'), hideSideMenuLabels: getHideSideMenuLabels().prop('checked'), tagsEnabled: getTagsEnabledCheckbox().prop('checked'), tagsText: getTagsTextArea().val(), tagPalette: getTagPaletteSelect().val(), tagPaletteCustom: getTagPaletteCustomTextArea().val().split(',').map(s => s.trim()), tagFont: getTagFontSelect().val(), tagHomePageLayout: getTagHomePageLayoutSelect().val(), tagContainerExtraBottomMargin: parseFloat(getTagContainerExtraBottomMarginInput().val()), tagLuminanceThreshold: parseFloat(getTagLuminanceThresholdInput().val()), tagBold: getTagBoldCheckbox().prop('checked'), tagItalic: getTagItalicCheckbox().prop('checked'), tagFontSize: parseFloat(getTagFontSizeInput().val()), tagIconSize: parseFloat(getTagIconSizeInput().val()), tagRoundness: parseFloat(getTagRoundnessInput().val()), tagTextYOffset: parseFloat(getTagTextYOffsetInput().val()), tagIconYOffset: parseFloat(getTagIconYOffsetInput().val()), tagToggleSave: getTagToggleSaveCheckbox().prop('checked'), toggleModeHooks: getToggleModeHooksCheckbox().prop('checked'), tagToggleModeIndicators: getTagToggleModeIndicatorsCheckbox().prop('checked'), debugMode: getEnableDebugCheckbox().prop('checked'), debugTagsMode: getEnableTagsDebugCheckbox().prop('checked'), debugTagsSuppressSubmit: getDebugTagsSuppressSubmitCheckbox().prop('checked'), autoOpenSettings: getAutoOpenSettingsCheckbox().prop('checked'), replaceIconsInMenu: getReplaceIconsInMenu().val(), 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(), modelLabelReasoningModelIconColor: getModelLabelReasoningModelIconColor().val(), modelLabelRemoveCpuIcon: getModelLabelRemoveCpuIconCheckbox().prop('checked'), modelLabelLargerIcons: getModelLabelLargerIconsCheckbox().prop('checked'), modelLabelIcons: getModelLabelIconsSelect().val(), customModelPopover: getCustomModelPopoverSelect().val(), mainCaptionHtml: getMainCaptionHtmlTextArea().val(), mainCaptionHtmlEnabled: getMainCaptionHtmlEnabledCheckbox().prop('checked'), customJs: getCustomJsTextArea().val(), customJsEnabled: getCustomJsEnabledCheckbox().prop('checked'), customCss: getCustomCssTextArea().val(), customCssEnabled: getCustomCssEnabledCheckbox().prop('checked'), customWidgetsHtml: getCustomWidgetsHtmlTextArea().val(), customWidgetsHtmlEnabled: getCustomWidgetsHtmlEnabledCheckbox().prop('checked'), leftMarginOfThreadContent: getLeftMarginOfThreadContentInput().val() === "" ? null : parseFloat(getLeftMarginOfThreadContentInput().val()), }; saveConfig(newConfig); }; 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 = pipe($iconButton.attr('href'))( dropStr(1), dropRightStr(1), ) || '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': 'font-sans text-xs md:text-xs text-textOff dark:text-textOffDark selection:bg-super/50 selection:text-textMain dark:selection:bg-superDuper/10 dark:selection:text-superDark', '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.OFF: return ''; 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 ?? ' '); case MODEL_LABEL_TEXT_MODE.VERY_SHORT: const abbr = modelDescription.abbrEn; if (!abbr) { console.warn('[getLabelFromModelDescription] modelDescription.abbrEn is empty', modelDescription); } else { return abbr; } const shortName = modelDescription.nameEnShort ?? modelDescription.nameEn; return shortName.split(/\s+/).map(word => word.charAt(0)).join(''); case MODEL_LABEL_TEXT_MODE.FAMILIAR_NAME: return modelDescription.familiarNameEn ?? modelDescription.nameEn; 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; case MODEL_LABEL_STYLE.NO_TEXT: return ''; 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); // Initial setup if elements don't exist yet if (!$el.find(`.${modelLabelCls}`).length) { $el.prepend(jq(`<span class="${modelLabelCls}"></span>`)); $el.closest('.col-start-3').removeClass('col-start-3').addClass('col-start-2 col-end-4'); } if (!$el.hasClass(modelIconButtonCls)) { $el.addClass(modelIconButtonCls); } // Get current config state and model information const modelDescription = PP.getModelDescriptionFromModelButton($el); const modelLabelFromAriaLabel = $el.attr('aria-label'); const modelLabel = config.modelLabelStyle === MODEL_LABEL_STYLE.NO_TEXT ? '' : getLabelFromModelDescription(config.modelLabelTextMode)(modelLabelFromAriaLabel)(modelDescription); if (modelLabel === undefined || modelLabel === null) { console.error('[handleModelLabel] modelLabel is empty', { modelDescription, modelLabelFromAriaLabel, $el }); return; } // Calculate the style classes const extraClasses = [ getExtraClassesFromModelLabelStyle(config.modelLabelStyle), config.modelLabelOverwriteCyanIconToGray ? modelLabelOverwriteCyanIconToGrayCls : '', ].filter(Boolean).join(' '); // Check the current "CPU icon removal" configuration state const shouldRemoveCpuIcon = config.modelLabelRemoveCpuIcon; const hasCpuIconRemoval = $el.hasClass(modelLabelRemoveCpuIconCls); // Only update CPU icon removal class if needed if (shouldRemoveCpuIcon !== hasCpuIconRemoval) { if (shouldRemoveCpuIcon) { $el.addClass(modelLabelRemoveCpuIconCls); } else { $el.removeClass(modelLabelRemoveCpuIconCls); } } // Handle larger icons setting const shouldUseLargerIcons = config.modelLabelLargerIcons; const hasLargerIconsClass = $el.hasClass(modelLabelLargerIconsCls); // Only update larger icons class if needed if (shouldUseLargerIcons !== hasLargerIconsClass) { if (shouldUseLargerIcons) { $el.addClass(modelLabelLargerIconsCls); } else { $el.removeClass(modelLabelLargerIconsCls); } } // Work with the label element const $label = $el.find(`.${modelLabelCls}`); // Use data attributes to track current state const storedModelDescriptionStr = $label.attr('data-model-description'); const storedExtraClasses = $label.attr('data-extra-classes'); const storedLabel = $label.attr('data-label-text'); // Only update if something has changed const modelDescriptionStr = JSON.stringify(modelDescription); const needsUpdate = storedModelDescriptionStr !== modelDescriptionStr || storedExtraClasses !== extraClasses || storedLabel !== modelLabel; if (needsUpdate) { // Store the current state in data attributes $label.attr('data-model-description', modelDescriptionStr); $label.attr('data-extra-classes', extraClasses); $label.attr('data-label-text', modelLabel); // Apply the text content $label.text(modelLabel); // Apply classes only if they've changed if (storedExtraClasses !== extraClasses) { $label.removeClass(modelLabelStyleButtonSubtleCls) .removeClass(modelLabelStyleButtonWhiteCls) .removeClass(modelLabelStyleButtonCyanCls) .removeClass(modelLabelOverwriteCyanIconToGrayCls) .addClass(extraClasses); } } // Handle error icon if errorType exists const hasErrorType = modelDescription?.errorType !== undefined; const existingErrorIcon = $el.find(`.${errorIconCls}`); // Check if we need to add or remove the error icon if (hasErrorType && existingErrorIcon.length === 0) { // Add the error icon const errorIconUrl = getLucideIconUrl('alert-triangle'); const $errorIcon = jq(`<img src="${errorIconUrl}" alt="Error" class="${errorIconCls}" />`) .attr('data-error-type', modelDescription.errorType) .css('filter', hexToCssFilter('#FFA500').filter) .attr('title', modelDescription.errorString || 'Error: Used fallback model'); // Insert the error icon at the correct position const $reasoningModelIcon = $el.find(`.${reasoningModelCls}`); if ($reasoningModelIcon.length > 0) { $reasoningModelIcon.after($errorIcon); } else { $el.prepend($errorIcon); } } else if (!hasErrorType && existingErrorIcon.length > 0) { // Remove the error icon if no longer needed existingErrorIcon.remove(); } else if (hasErrorType && existingErrorIcon.length > 0) { // Update the error icon title if it changed if (existingErrorIcon.attr('data-error-type') !== modelDescription.errorType) { existingErrorIcon .attr('data-error-type', modelDescription.errorType) .attr('title', modelDescription.errorString || 'Error: Used fallback model'); } } // Handle model icon if (config.modelLabelIcons && config.modelLabelIcons !== MODEL_LABEL_ICONS.OFF) { const existingIcon = $el.find(`.${modelIconCls}`); // Get model-specific icon based on model name const modelName = modelDescription?.nameEn ?? ''; const brandIconInfo = getBrandIconInfo(modelName); if (!brandIconInfo) { // TODO: very spammy, issues with "models" like "Pro Search", "Deep Research" and "Labs" debugLog('brandIconInfo is null', { modelName, modelDescription }); return; } const { iconName, brandColor } = brandIconInfo; const existingIconData = existingIcon.attr('data-model-icon'); const existingIconMode = existingIcon.attr('data-icon-mode'); // Check if we need to update the icon const shouldUpdateIcon = existingIconData !== iconName || existingIcon.length === 0 || existingIconMode !== config.modelLabelIcons; if (shouldUpdateIcon) { existingIcon.remove(); if (iconName) { const iconUrl = getLobeIconsUrl(iconName); const $icon = jq(`<img src="${iconUrl}" alt="Model icon" class="${modelIconCls}" />`) .attr('data-model-icon', iconName) .attr('data-icon-mode', config.modelLabelIcons); // Apply styling based on monochrome/color mode if (config.modelLabelIcons === MODEL_LABEL_ICONS.MONOCHROME) { // Apply monochrome filter $icon.css('filter', 'invert(1)'); // Apply color classes for monochrome icons based on button style if (config.modelLabelStyle === MODEL_LABEL_STYLE.JUST_TEXT) { $icon.addClass(iconColorGrayCls); } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_CYAN) { $icon.addClass(iconColorCyanCls); } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_SUBTLE) { $icon.addClass(iconColorGrayCls); } else if (config.modelLabelStyle === MODEL_LABEL_STYLE.BUTTON_WHITE) { $icon.addClass(iconColorWhiteCls); } } else if (config.modelLabelIcons === MODEL_LABEL_ICONS.COLOR) { // Ensure the icon displays in color $icon.attr('data-brand-color', brandColor); $icon.css('filter', hexToCssFilter(brandColor).filter); $icon.attr('data-brand-color-filter', hexToCssFilter(brandColor).filter); } const $reasoningModelIcon = $el.find(`.${reasoningModelCls}`); const $errorIcon = $el.find(`.${errorIconCls}`); const hasReasoningModelIcon = $reasoningModelIcon.length !== 0; const hasErrorIcon = $errorIcon.length !== 0; if (hasReasoningModelIcon) { // $icon.css({ marginLeft: '0px' }); // $el.css({ paddingRight: hasReasoningModelIcon ? '8px' : '2px' }); $reasoningModelIcon.after($icon); } else if (hasErrorIcon) { $errorIcon.after($icon); } else { // $icon.css({ marginLeft: '-2px' }); $el.prepend($icon); } // if (!modelLabel) { // $icon.css({ marginRight: '-6px', marginLeft: '-2px' }); // $el.css({ paddingRight: '8px', paddingLeft: '10px' }); // } } } } else { // Remove model icon if setting is off $el.find(`.${modelIconCls}`).remove(); } // Handle reasoning model icon const isReasoningModel = modelDescription?.modelType === 'reasoning'; if (config.modelLabelUseIconForReasoningModels !== MODEL_LABEL_ICON_REASONING_MODEL.OFF) { const prevReasoningModelIcon = $el.find(`.${reasoningModelCls}`); const hasIconSetting = $el.attr('data-reasoning-icon-setting'); const currentSetting = config.modelLabelUseIconForReasoningModels; const currentIconColor = config.modelLabelReasoningModelIconColor || '#ffffff'; const storedIconColor = $el.attr('data-reasoning-icon-color'); // Only make changes if the reasoning status, icon setting, or color has changed if (hasIconSetting !== currentSetting || (isReasoningModel && prevReasoningModelIcon.length === 0) || (!isReasoningModel && prevReasoningModelIcon.length > 0) || storedIconColor !== currentIconColor) { // Update tracking attributes $el.attr('data-reasoning-icon-setting', currentSetting); $el.attr('data-reasoning-icon-color', currentIconColor); $el.attr('data-is-reasoning-model', isReasoningModel); // Update reasoning model class as needed if (!isReasoningModel) { $el.addClass(notReasoningModelCls); prevReasoningModelIcon.remove(); } else { $el.removeClass(notReasoningModelCls); if (prevReasoningModelIcon.length === 0) { const iconUrl = getLucideIconUrl(config.modelLabelUseIconForReasoningModels.toLowerCase().replace(' ', '-')); const $icon = jq(`<img src="${iconUrl}" alt="Reasoning model" class="${reasoningModelCls}" />`); $icon.css('filter', hexToCssFilter(config.modelLabelReasoningModelIconColor || '#ffffff').filter); $el.prepend($icon); const $reasoningModelIcon = $el.find(`.${reasoningModelCls}`); $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); } const $modelLabelIcon = $el.find(`.${modelIconCls}`); const $errorIcon = $el.find(`.${errorIconCls}`); if ($modelLabelIcon.length !== 0 || $errorIcon.length !== 0) { $reasoningModelIcon.css({ marginLeft: '4px' }); } else { $reasoningModelIcon.css({ marginLeft: '0px' }); } } else { prevReasoningModelIcon.css('filter', hexToCssFilter(config.modelLabelReasoningModelIconColor || '#ffffff').filter); } } } } }); }; 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('.cursor-pointer > .flex').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.mainCaptionHtmlEnabled) return; if (PP.getMainCaption().hasClass(mainCaptionAppliedCls)) return; PP.setMainCaptionHtml(config.mainCaptionHtml); PP.getMainCaption().addClass(mainCaptionAppliedCls); }; const handleCustomJs = () => { const config = loadConfigOrDefault(); if (!config.customJsEnabled) 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.customCssEnabled) 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.customWidgetsHtmlEnabled) 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().append(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 handleRemoveWhiteSpaceOnLeftOfThreadContent = () => { const config = loadConfigOrDefault(); const val = parseFloat(config.leftMarginOfThreadContent); if (isNaN(val)) return; if (jq('head').find(`#${leftMarginOfThreadContentStylesId}`).length > 0) return; jq(`<style id="${leftMarginOfThreadContentStylesId}">.max-w-threadContentWidth { margin-left: ${val}em !important; }</style>`).appendTo("head"); }; // Function to apply a tag's actions (works for both regular and toggle tags) const applyTagActions = async (tag, options = {}) => { const { skipText = false, callbacks = {} } = options; debugLog('Applying tag actions for tag:', tag); // Apply mode setting if (tag.setMode) { const mode = tag.setMode.toLowerCase(); if (mode === 'pro' || mode === 'research' || mode === 'deep-research' || mode === 'dr' || mode === 'lab') { // Convert aliases to the actual mode name that PP understands const normalizedMode = mode === 'dr' || mode === 'deep-research' ? 'research' : mode; try { await PP.doSelectQueryMode(normalizedMode); debugLog(`[applyTagActions]: Set mode to ${normalizedMode}`); wait(50); } catch (error) { debugLog(`[applyTagActions]: Error setting mode to ${normalizedMode}`, error); } } else { debugLog(`[applyTagActions]: Invalid mode: ${tag.setMode}`); } } // Apply model setting if (tag.setModel) { try { const modelDescriptor = PP.getModelDescriptorFromId(tag.setModel); debugLog('[applyTagActions]: set model=', tag.setModel, ' modelDescriptor=', modelDescriptor); if (modelDescriptor) { await PP.doSelectModel(modelDescriptor.index); debugLog(`[applyTagActions]: Selected model ${modelDescriptor.nameEn}`); if (callbacks.modelSet) callbacks.modelSet(modelDescriptor); } else { debugLog(`[applyTagActions]: Model descriptor not found for ${tag.setModel}`); } } catch (error) { debugLog(`[applyTagActions]: Error setting model to ${tag.setModel}`, error); } } // Apply sources setting if (tag.setSources) { try { // Use PP's high-level function that handles the whole process await PP.doSetSourcesSelectionListValues()(tag.setSources); debugLog(`[applyTagActions]: Sources set to ${tag.setSources}`); await PP.sleep(50); if (callbacks.sourcesSet) callbacks.sourcesSet(); } catch (error) { logError(`[applyTagActions]: Error setting sources`, error); } } // Add text to prompt if it's not empty and we're not skipping text if (!skipText && tag.text && tag.text.trim().length > 0) { try { const promptArea = PP.getAnyPromptArea(); if (promptArea.length) { const promptAreaRaw = promptArea[0]; const newText = applyTagToString(tag, promptArea.val(), promptAreaRaw.selectionStart); changeValueUsingEvent(promptAreaRaw, newText); debugLog(`[applyTagActions]: Applied text: "${tag.text.substring(0, 20)}${tag.text.length > 20 ? '...' : ''}"`); if (callbacks.textApplied) callbacks.textApplied(newText); } else { debugLog(`[applyTagActions]: No prompt area found for text insertion`); } } catch (error) { debugLog(`[applyTagActions]: Error applying text`, error); } } }; // Function to apply toggled tags' actions when submit is clicked const applyToggledTagsOnSubmit = async ($wrapper) => { debugLog('Applying toggled tags on submit', { $wrapper }); const config = loadConfigOrDefault(); const allTags = parseTagsText(config.tagsText ?? defaultConfig.tagsText); const currentContainerType = getPromptWrapperTagContainerType($wrapper); if (!currentContainerType) { logError('Could not determine current container type, skipping toggled tags application', { $wrapper, currentContainerType, allTags, }); return false; } // Find all toggled tags that are relevant for the current container type const toggledTags = allTags.filter(tag => { // First check if it's a toggle tag if (!tag.toggleMode) return false; const tagId = generateToggleTagId(tag); if (!tagId) return false; // Check in-memory toggle state first const inMemoryToggled = window._phTagToggleState && window._phTagToggleState[tagId] === true; // Then fall back to saved state if tagToggleSave is enabled const savedToggled = config.tagToggleSave && config.tagToggledStates && config.tagToggledStates[tagId] === true; // If neither is toggled, return false if (!inMemoryToggled && !savedToggled) return false; // Then check if this tag is relevant for the current container return isTagRelevantForContainer(currentContainerType)(tag); }); debugLog(`Toggled tags for ${currentContainerType} context:`, toggledTags.length); // Apply each toggled tag's actions sequentially, waiting for each to complete for (const tag of toggledTags) { debugLog(`Applying toggled tag: ${tag.label || 'Unnamed tag'}`); try { await applyTagActions(tag); debugLog(`Successfully applied toggled tag: ${tag.label || 'Unnamed tag'}`); } catch (error) { logError(`Error applying toggled tag: ${tag.label || 'Unnamed tag'}`, error); } } return toggledTags.length > 0; }; // Function to check if there are active toggled tags const hasActiveToggledTags = () => { const config = loadConfigOrDefault(); // Check in-memory toggle states first if (window._phTagToggleState && Object.values(window._phTagToggleState).some(state => state === true)) { return true; } // Then check saved toggle states if enabled if (!config.tagToggleSave || !config.tagToggledStates) return false; // Check if any tags are toggled on in saved state return Object.values(config.tagToggledStates).some(state => state === true); }; // Function to check if there are active toggled tags for the current context const hasActiveToggledTagsForCurrentContext = ($wrapper) => { const config = loadConfigOrDefault(); const currentContainerType = getPromptWrapperTagContainerType($wrapper); // START DEBUG LOGGING const wrapperId = $wrapper && $wrapper.length ? ($wrapper.attr('id') || 'wrapper-' + Math.random().toString(36).substring(2, 9)) : 'no-wrapper'; if (!$wrapper || !$wrapper.length) { debugLogTags(`hasActiveToggledTagsForCurrentContext - No valid wrapper provided for ${wrapperId}`); return false; } if (!currentContainerType) { debugLogTags(`hasActiveToggledTagsForCurrentContext - No container type for wrapper ${wrapperId}`); return false; } debugLogTags(`hasActiveToggledTagsForCurrentContext - Container type ${currentContainerType} for wrapper ${wrapperId}`); // END DEBUG LOGGING // Get all tags const allTags = parseTagsText(config.tagsText ?? defaultConfig.tagsText); // Filter for toggled-on tags relevant to the current context const hasActiveTags = allTags.some(tag => { if (!tag.toggleMode) return false; const tagId = generateToggleTagId(tag); if (!tagId) return false; // Check in-memory toggle state first const inMemoryToggled = window._phTagToggleState && window._phTagToggleState[tagId] === true; // Then fall back to saved state if tagToggleSave is enabled const savedToggled = config.tagToggleSave && config.tagToggledStates && config.tagToggledStates[tagId] === true; // If neither is toggled, return false if (!inMemoryToggled && !savedToggled) return false; // Check if this tag is relevant for the current container const isRelevant = isTagRelevantForContainer(currentContainerType)(tag); // DEBUG LOG if (inMemoryToggled || savedToggled) { debugLogTags(`hasActiveToggledTagsForCurrentContext - Tag ${tag.label || 'unnamed'}: inMemory=${inMemoryToggled}, saved=${savedToggled}, relevant=${isRelevant}`); } return isRelevant; }); // DEBUG LOG debugLogTags(`hasActiveToggledTagsForCurrentContext - Final result for ${wrapperId}: ${hasActiveTags}`); return hasActiveTags; }; // Function to get a comma-separated list of active toggled tag labels const getActiveToggledTagLabels = ($wrapper) => { const config = loadConfigOrDefault(); // Get all tags const allTags = parseTagsText(config.tagsText ?? defaultConfig.tagsText); // Filter for toggled-on tags const activeTags = allTags.filter(tag => { if (!tag.toggleMode) return false; const tagId = generateToggleTagId(tag); if (!tagId) return false; // Check in-memory toggle state first const inMemoryToggled = window._phTagToggleState && window._phTagToggleState[tagId] === true; // Then fall back to saved state if tagToggleSave is enabled const savedToggled = config.tagToggleSave && config.tagToggledStates && config.tagToggledStates[tagId] === true; // If neither is toggled, return false if (!inMemoryToggled && !savedToggled) return false; // If wrapper is provided, check if this tag is relevant for the current container type if ($wrapper) { const currentContainerType = getPromptWrapperTagContainerType($wrapper); if (currentContainerType && !isTagRelevantForContainer(currentContainerType)(tag)) { return false; } } return true; }); // Return labels joined by commas return activeTags.map(tag => tag.label || 'Unnamed tag').join(', '); }; const mockChromeRuntime = () => { if (!window.chrome) { window.chrome = {}; } if (!window.chrome.runtime) { window.chrome.runtime = { _about: 'mock by Perplexity Helper; otherwise clicking on the submit button programmatically crashes in promise', sendMessage: function() { log('mockChromeRuntime: sendMessage', arguments); return Promise.resolve({success: true}); } }; } }; // Enhanced submit button for toggled tags const createEnhancedSubmitButton = (originalButton) => { const $originalBtn = jq(originalButton); const config = loadConfigOrDefault(); // Find the proper prompt area wrapper, going up to queryBox class first const $queryBox = $originalBtn.closest(`.${queryBoxCls}`); const $wrapper = $queryBox.length ? $queryBox.parent() : $originalBtn.closest('.flex').parent().parent().parent(); const hasActiveInContext = hasActiveToggledTagsForCurrentContext($wrapper); const activeTagLabels = getActiveToggledTagLabels($wrapper); const title = activeTagLabels ? `Submit with toggled tags applied (${activeTagLabels})` : 'Submit with toggled tags applied'; const $enhancedBtn = jq('<div/>') .addClass(enhancedSubmitButtonCls) .attr('title', title) // ISSUE: Using hard-coded 'active' class instead of enhancedSubmitButtonActiveCls .toggleClass('active', hasActiveInContext && config.tagToggleModeIndicators) .html(`<span class="${enhancedSubmitButtonPhTextCls}">PH</span>`); // Add the enhanced button as an overlay on the original $originalBtn.css('position', 'relative'); $originalBtn.append($enhancedBtn); // Handle click on enhanced button $enhancedBtn.on('click', async (e) => { e.preventDefault(); e.stopPropagation(); // Show temporary processing indicator // ISSUE: Using hard-coded 'active' class instead of enhancedSubmitButtonActiveCls $enhancedBtn.addClass('active').css('opacity', '1').find(`.${enhancedSubmitButtonPhTextCls}`).text('...'); // DEBUG if (loadConfigOrDefault().debugTagsMode) { debugLogTags(`Enhanced button click - adding 'active' class (should be ${enhancedSubmitButtonActiveCls})`); } const finishProcessing = () => { if (loadConfigOrDefault().debugTagsSuppressSubmit) { log('Suppressing submit after applying tags'); return; } try { // $originalBtn[0].click(); // const event = new MouseEvent('click', { // bubbles: true, // cancelable: true, // }); // $originalBtn[0].dispatchEvent(event); // $originalBtn.trigger('click'); // Try to make a more authentic-looking click event // const clickEvent = new MouseEvent('click', { // bubbles: true, // cancelable: true, // view: window, // detail: 1, // number of clicks // isTrusted: true // attempt to make it look trusted (though this is readonly) // }); // $originalBtn[0].dispatchEvent(clickEvent); // Find the React component's props // const reactInstance = Object.keys($originalBtn[0]).find(key => key.startsWith('__reactFiber$')); // if (reactInstance) { // const props = $originalBtn[0][reactInstance].memoizedProps; // if (props && props.onClick) { // // Call the handler directly, bypassing the event system // props.onClick(); // } else { // logError('[createEnhancedSubmitButton]: No onClick handler found', { // $originalBtn, // reactInstance, // props, // }); // } // } else { // logError('[createEnhancedSubmitButton]: No React instance found', { // $originalBtn, // }); // } mockChromeRuntime(); $originalBtn.trigger('click'); } catch (error) { logError('[createEnhancedSubmitButton]: Error in finishProcessing:', error); } }; try { // Apply all toggled tags sequentially, waiting for each to complete const tagsApplied = await applyToggledTagsOnSubmit($wrapper); // Add a small delay after applying all tags to ensure UI updates are complete if (tagsApplied) { await PP.sleep(50); } // Reset the button appearance $enhancedBtn.css('opacity', '').find(`.${enhancedSubmitButtonPhTextCls}`).text(''); if (!hasActiveInContext) { // ISSUE: Using hard-coded 'active' class instead of enhancedSubmitButtonActiveCls $enhancedBtn.removeClass('active'); // DEBUG if (loadConfigOrDefault().debugTagsMode) { debugLogTags(`Enhanced button - removing 'active' class because !hasActiveInContext (should be ${enhancedSubmitButtonActiveCls})`); } } // Trigger the original button click finishProcessing(); } catch (error) { console.error('Error in enhanced submit button:', error); $enhancedBtn.css('opacity', '').find(`.${enhancedSubmitButtonPhTextCls}`).text(''); if (!hasActiveInContext) { // ISSUE: Using hard-coded 'active' class instead of enhancedSubmitButtonActiveCls $enhancedBtn.removeClass('active'); // DEBUG if (loadConfigOrDefault().debugTagsMode) { debugLogTags(`Enhanced button error handler - removing 'active' class (should be ${enhancedSubmitButtonActiveCls})`); } } // Still attempt to submit even if there was an error finishProcessing(); } }); return $enhancedBtn; }; // Add enhanced submit buttons to handle toggled tags const patchSubmitButtonsForToggledTags = () => { const config = loadConfigOrDefault(); // Skip if toggle mode hooks are disabled if (!config.toggleModeHooks) return; const submitButtons = PP.getSubmitButtonAnyExceptMic(); if (!submitButtons.length) return; submitButtons.each((_, btn) => { const $btn = jq(btn); if ($btn.attr('data-patched-for-toggled-tags')) return; // Create our enhanced button overlay createEnhancedSubmitButton(btn); // Mark as patched $btn.attr('data-patched-for-toggled-tags', 'true'); }); }; // Function to add keypress listeners to prompt areas const updateTextareaIndicator = ($textarea) => { if (!$textarea || !$textarea.length) return; // Get the current config const config = loadConfigOrDefault(); // Get the wrapper const $wrapper = PP.getAnyPromptAreaWrapper($textarea.nthParent(4)); if (!$wrapper || !$wrapper.length) return; // Check for active toggled tags in this context const hasActiveInContext = hasActiveToggledTagsForCurrentContext($wrapper); // Should we show the indicator? const shouldShowIndicator = hasActiveInContext && config.tagToggleModeIndicators; // Get current state to avoid unnecessary DOM updates const currentlyHasClass = $textarea.hasClass(promptAreaKeyListenerCls); const currentlyHasIndicator = $textarea.siblings(`.${promptAreaKeyListenerIndicatorCls}`).length > 0; // Only update DOM if state has changed if (currentlyHasClass !== shouldShowIndicator || currentlyHasIndicator !== shouldShowIndicator) { if (shouldShowIndicator) { // Apply the class for the glow effect with transition if not already applied if (!currentlyHasClass) { $textarea.addClass(promptAreaKeyListenerCls); } // Add the pulse dot indicator if not already present if (!currentlyHasIndicator) { // Make sure parent has relative positioning for proper indicator positioning const $parent = $textarea.parent(); if ($parent.css('position') !== 'relative') { $parent.css('position', 'relative'); } const $indicator = jq('<div>') .addClass(promptAreaKeyListenerIndicatorCls) .attr('title', 'Toggle tags active - Press Enter to submit'); $textarea.after($indicator); // Force a reflow then add visible class for animation $indicator[0].offsetHeight; // Force reflow $indicator.addClass('visible'); } } else { // Remove the class with transition for fade out if (currentlyHasClass) { $textarea.removeClass(promptAreaKeyListenerCls); } // For indicator, first make it invisible with transition, then remove from DOM if (currentlyHasIndicator) { const $indicator = $textarea.siblings(`.${promptAreaKeyListenerIndicatorCls}`); $indicator.removeClass('visible'); // Remove from DOM after transition completes setTimeout(() => { if ($indicator.length) $indicator.remove(); }, 500); // Match the transition duration in CSS } } } }; const addPromptAreaKeyListeners = () => { const config = loadConfigOrDefault(); // Skip if toggle mode hooks are disabled if (!config.toggleModeHooks) return; // Get all prompt areas const promptAreas = PP.getAnyPromptArea(); if (!promptAreas.length) return; // Process textareas that don't have listeners yet promptAreas.each((_, textarea) => { const $textarea = jq(textarea); // Skip if already has a listener if ($textarea.attr('data-toggle-keypress-listener')) return; // Mark as having a listener to avoid duplicates $textarea.attr('data-toggle-keypress-listener', 'true'); // Add the visual indicator if needed updateTextareaIndicator($textarea); // Add the keypress listener for Enter key $textarea.on('keydown.togglehook', (e) => { // Only handle Enter key if (e.key === 'Enter' && !e.shiftKey) { // Find the wrapper const $wrapper = PP.getAnyPromptAreaWrapper($textarea.nthParent(4)); if (!$wrapper || !$wrapper.length) return; // Check if there are active toggled tags for this context const hasActiveInContext = hasActiveToggledTagsForCurrentContext($wrapper); if (!hasActiveInContext) return; // Prevent default behavior e.preventDefault(); e.stopPropagation(); // Flash the textarea indicator with animation that always plays $textarea.removeClass(pulseFocusCls); $textarea[0].offsetHeight; // Force reflow to ensure animation plays $textarea.addClass(pulseFocusCls); setTimeout(() => $textarea.removeClass(pulseFocusCls), 400); // Find and click the submit button const $submitBtn = PP.submitButtonAny($wrapper); // If we found a submit button with an enhanced button overlay, use that if ($submitBtn.length && $submitBtn.find(`.${enhancedSubmitButtonCls}`).length) { $submitBtn.find(`.${enhancedSubmitButtonCls}`).click(); } else if ($submitBtn.length) { // Otherwise use the regular submit button $submitBtn.click(); } return false; } }); // Add focus handling to update appearance $textarea.on('focus.togglehook', () => { // Update indicator on focus updateTextareaIndicator($textarea); }); }); }; const updateToggleIndicators = () => { const config = loadConfigOrDefault(); // Track state changes with this object for debugging const debugStateChanges = { totalButtons: 0, unchanged: 0, titleChanged: 0, activeStateChanged: 0, stateChanges: [] }; // Update all enhanced submit buttons individually jq(`.${enhancedSubmitButtonCls}`).each((idx, btn) => { const $btn = jq(btn); const $originalBtn = $btn.parent(); const btnId = $btn.attr('id') || `btn-${idx}`; debugStateChanges.totalButtons++; // Find the proper prompt area wrapper, going up to queryBox class first const $queryBox = $originalBtn.closest(`.${queryBoxCls}`); const $wrapper = $queryBox.length ? $queryBox.parent() : $originalBtn.closest('.flex').parent().parent().parent(); // DEBUGGING - Track button's wrapper const wrapperId = $wrapper && $wrapper.length ? ($wrapper.attr('id') || 'wrapper-' + Math.random().toString(36).substring(2, 9)) : 'no-wrapper'; if (loadConfigOrDefault().debugTagsMode) { debugLogTags(`updateToggleIndicators - Button ${btnId} in wrapper ${wrapperId}`); } const hasActiveInContext = hasActiveToggledTagsForCurrentContext($wrapper); const activeTagLabels = getActiveToggledTagLabels($wrapper); const title = activeTagLabels ? `Submit with toggled tags applied (${activeTagLabels})` : 'Submit with toggled tags applied'; // Get current state to avoid unnecessary DOM updates // ISSUE: using hard-coded 'active' class instead of generated enhancedSubmitButtonActiveCls const isCurrentlyActive = $btn.hasClass('active'); const shouldBeActive = hasActiveInContext && config.tagToggleModeIndicators; // DEBUG - Log the class mismatch if (loadConfigOrDefault().debugTagsMode) { const hasGeneratedClass = $btn.hasClass(enhancedSubmitButtonActiveCls); if (isCurrentlyActive !== hasGeneratedClass) { debugLogTags(`Class mismatch detected for ${btnId}: 'active'=${isCurrentlyActive}, '${enhancedSubmitButtonActiveCls}'=${hasGeneratedClass}`); } } const currentTitle = $btn.attr('title'); // DEBUGGING - Track state for this button const stateChange = { btnId, wrapperId, isCurrentlyActive, shouldBeActive, stateChanged: isCurrentlyActive !== shouldBeActive, titleChanged: currentTitle !== title }; debugStateChanges.stateChanges.push(stateChange); // Only update DOM elements if state has actually changed if (isCurrentlyActive !== shouldBeActive || currentTitle !== title) { // Update title if changed if (currentTitle !== title) { debugStateChanges.titleChanged++; $btn.attr('title', title); } // Toggle active class with transition effect if state has changed if (isCurrentlyActive !== shouldBeActive) { debugStateChanges.activeStateChanged++; // ISSUE: We're using literal 'active' here instead of enhancedSubmitButtonActiveCls // This should be fixed to use the generated class, but we're just logging for now // No additional class manipulation needed - CSS transitions handle the animation $btn.toggleClass('active', shouldBeActive); if (loadConfigOrDefault().debugTagsMode) { debugLogTags(`Class toggle for ${btnId}: 'active' changed to ${shouldBeActive}, from ${isCurrentlyActive}`); } // If transitioning to active, ensure we have proper z-index to show over other elements if (shouldBeActive) { $originalBtn.css('z-index', '5'); } else { // Reset z-index after transition setTimeout(() => $originalBtn.css('z-index', ''), 500); } } // Update outline only if debugging state requires it $btn.css({ outline: config.debugTagsSuppressSubmit ? '5px solid red' : 'none' }); } else { debugStateChanges.unchanged++; } }); // Log state change stats if (loadConfigOrDefault().debugTagsMode) { if (debugStateChanges.activeStateChanged > 0) { debugLogTags(`updateToggleIndicators - SUMMARY: total=${debugStateChanges.totalButtons}, unchanged=${debugStateChanges.unchanged}, titleChanged=${debugStateChanges.titleChanged}, activeStateChanged=${debugStateChanges.activeStateChanged}`); debugLogTags('updateToggleIndicators - State changes:', debugStateChanges.stateChanges.filter(sc => sc.stateChanged)); } } // Also update all textarea indicators when toggle mode hooks are enabled if (config.toggleModeHooks) { // Get all prompt areas with keypress listeners const promptAreas = jq('textarea[data-toggle-keypress-listener="true"]'); if (promptAreas.length) { promptAreas.each((_, textarea) => { updateTextareaIndicator(jq(textarea)); }); } } }; // Function to reset all toggle states (both in-memory and saved if tagToggleSave is enabled) const resetAllToggleStates = () => { // Reset in-memory state window._phTagToggleState = {}; // Reset saved state if tagToggleSave is enabled const config = loadConfigOrDefault(); if (config.tagToggleSave && config.tagToggledStates) { const updatedConfig = { ...config, tagToggledStates: {} }; saveConfig(updatedConfig); } // Update existing toggle tags directly in the DOM if possible const existingToggledTags = jq(`.${tagCls}[data-toggled="true"]`); if (existingToggledTags.length > 0) { existingToggledTags.each((_, el) => { const $el = jq(el); const tagData = JSON.parse($el.attr('data-tag') || '{}'); if (tagData) { // Reset visual state back to untoggled updateToggleTagState($el, tagData, false); } }); } else { // If we couldn't find any toggled tags in the DOM (perhaps they were added after), // fall back to a full refresh refreshTags({ force: true }); } // Update indicators updateToggleIndicators(); }; // Function to generate a consistent ID for toggle tags const generateToggleTagId = tag => { if (!tag.toggleMode) return null; return `toggle:${(tag.label || '') + ':' + (tag.position || '') + ':' + (tag.color || '')}:${tag.originalIndex || 0}`; }; const work = () => { handleModalCreation(); handleTopSettingsButtonInsertion(); handleTopSettingsButtonSetup(); handleSettingsInit(); handleLeftSettingsButtonSetup(); handleExtraSpaceBellowLastAnswer(); handleHideDiscoverButton(); handleHideSideMenuLabels(); handleRemoveWhiteSpaceOnLeftOfThreadContent(); updateToggleIndicators(); patchSubmitButtonsForToggledTags(); addPromptAreaKeyListeners(); 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', 'JetBrains Mono': 'https://fonts.cdnfonts.com/css/jetbrains-mono', }; 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()?.debugMode) { enableDebugMode(); } debugLog('TAGS_PALETTES', TAGS_PALETTES); if (loadConfigOrDefault()?.debugTagsMode) { enableTagsDebugging(); } // Initialize in-memory toggle state from saved state if tagToggleSave is enabled const config = loadConfigOrDefault(); if (config.tagToggleSave && config.tagToggledStates) { window._phTagToggleState = { ...config.tagToggledStates }; debugLog('Initialized in-memory toggle state from saved state', window._phTagToggleState); } else { window._phTagToggleState = {}; } 'use strict'; jq("head").append(`<style>${styles}</style>`); setupTags(); setupFixImageGenerationOverlay(); const mainInterval = setInterval(work, 1000); // This interval is too fast (100ms) which causes frequent DOM updates // and leads to the class toggling issue with 'active' vs enhancedSubmitButtonActiveCls 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); loadFont('JetBrains Mono'); // Auto open settings if enabled if (loadConfigOrDefault()?.autoOpenSettings) { // Use setTimeout to ensure the DOM is ready setTimeout(() => { showPerplexityHelperSettingsModal(); }, 1000); } 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()'); }());
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址